github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/worker/uniter/uniter_test.go (about)

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter_test
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/juju/errors"
    19  	jc "github.com/juju/testing/checkers"
    20  	ft "github.com/juju/testing/filetesting"
    21  	"github.com/juju/utils/clock"
    22  	gc "gopkg.in/check.v1"
    23  	corecharm "gopkg.in/juju/charm.v5"
    24  
    25  	"github.com/juju/juju/agent/tools"
    26  	"github.com/juju/juju/apiserver/params"
    27  	"github.com/juju/juju/juju/testing"
    28  	"github.com/juju/juju/state"
    29  	"github.com/juju/juju/testcharms"
    30  	coretesting "github.com/juju/juju/testing"
    31  	"github.com/juju/juju/worker/uniter"
    32  	"github.com/juju/juju/worker/uniter/operation"
    33  )
    34  
    35  type UniterSuite struct {
    36  	coretesting.GitSuite
    37  	testing.JujuConnSuite
    38  	dataDir  string
    39  	oldLcAll string
    40  	unitDir  string
    41  
    42  	collectMetricsTicker   *uniter.ManualTicker
    43  	sendMetricsTicker      *uniter.ManualTicker
    44  	updateStatusHookTicker *uniter.ManualTicker
    45  }
    46  
    47  var _ = gc.Suite(&UniterSuite{})
    48  
    49  var leaseClock *coretesting.Clock
    50  
    51  // This guarantees that we get proper platform
    52  // specific error directly from their source
    53  // This works on both windows and unix
    54  var errNotDir = syscall.ENOTDIR.Error()
    55  
    56  func (s *UniterSuite) SetUpSuite(c *gc.C) {
    57  	s.GitSuite.SetUpSuite(c)
    58  	s.JujuConnSuite.SetUpSuite(c)
    59  	s.dataDir = c.MkDir()
    60  	toolsDir := tools.ToolsDir(s.dataDir, "unit-u-0")
    61  	err := os.MkdirAll(toolsDir, 0755)
    62  	c.Assert(err, jc.ErrorIsNil)
    63  	// TODO(fwereade) GAAAAAAAAAAAAAAAAAH this is LUDICROUS.
    64  	cmd := exec.Command("go", "build", "github.com/juju/juju/cmd/jujud")
    65  	cmd.Dir = toolsDir
    66  	out, err := cmd.CombinedOutput()
    67  	c.Logf(string(out))
    68  	c.Assert(err, jc.ErrorIsNil)
    69  	s.oldLcAll = os.Getenv("LC_ALL")
    70  	os.Setenv("LC_ALL", "en_US")
    71  	s.unitDir = filepath.Join(s.dataDir, "agents", "unit-u-0")
    72  
    73  	zone, err := time.LoadLocation("")
    74  	c.Assert(err, jc.ErrorIsNil)
    75  	now := time.Date(2030, 11, 11, 11, 11, 11, 11, zone)
    76  	leaseClock = coretesting.NewClock(now)
    77  	oldGetClock := state.GetClock
    78  	state.GetClock = func() clock.Clock {
    79  		return leaseClock
    80  	}
    81  	s.AddSuiteCleanup(func(*gc.C) { state.GetClock = oldGetClock })
    82  }
    83  
    84  func (s *UniterSuite) TearDownSuite(c *gc.C) {
    85  	os.Setenv("LC_ALL", s.oldLcAll)
    86  	s.JujuConnSuite.TearDownSuite(c)
    87  	s.GitSuite.TearDownSuite(c)
    88  }
    89  
    90  func (s *UniterSuite) SetUpTest(c *gc.C) {
    91  	s.collectMetricsTicker = uniter.NewManualTicker()
    92  	s.sendMetricsTicker = uniter.NewManualTicker()
    93  	s.updateStatusHookTicker = uniter.NewManualTicker()
    94  	s.GitSuite.SetUpTest(c)
    95  	s.JujuConnSuite.SetUpTest(c)
    96  	s.PatchValue(uniter.IdleWaitTime, 1*time.Millisecond)
    97  }
    98  
    99  func (s *UniterSuite) TearDownTest(c *gc.C) {
   100  	s.ResetContext(c)
   101  	s.JujuConnSuite.TearDownTest(c)
   102  	s.GitSuite.TearDownTest(c)
   103  }
   104  
   105  func (s *UniterSuite) Reset(c *gc.C) {
   106  	s.JujuConnSuite.Reset(c)
   107  	s.ResetContext(c)
   108  }
   109  
   110  func (s *UniterSuite) ResetContext(c *gc.C) {
   111  	err := os.RemoveAll(s.unitDir)
   112  	c.Assert(err, jc.ErrorIsNil)
   113  }
   114  
   115  func (s *UniterSuite) runUniterTests(c *gc.C, uniterTests []uniterTest) {
   116  	for i, t := range uniterTests {
   117  		c.Logf("\ntest %d: %s\n", i, t.summary)
   118  		func() {
   119  			defer s.Reset(c)
   120  			env, err := s.State.Environment()
   121  			c.Assert(err, jc.ErrorIsNil)
   122  			ctx := &context{
   123  				s:                      s,
   124  				st:                     s.State,
   125  				uuid:                   env.UUID(),
   126  				path:                   s.unitDir,
   127  				dataDir:                s.dataDir,
   128  				charms:                 make(map[string][]byte),
   129  				collectMetricsTicker:   s.collectMetricsTicker,
   130  				sendMetricsTicker:      s.sendMetricsTicker,
   131  				updateStatusHookTicker: s.updateStatusHookTicker,
   132  			}
   133  			ctx.run(c, t.steps)
   134  		}()
   135  	}
   136  }
   137  
   138  func (s *UniterSuite) TestUniterStartup(c *gc.C) {
   139  	s.runUniterTests(c, []uniterTest{
   140  		// Check conditions that can cause the uniter to fail to start.
   141  		ut(
   142  			"unable to create state dir",
   143  			writeFile{"state", 0644},
   144  			createCharm{},
   145  			createServiceAndUnit{},
   146  			startUniter{},
   147  			waitUniterDead{err: `failed to initialize uniter for "unit-u-0": .*` + errNotDir},
   148  		), ut(
   149  			"unknown unit",
   150  			// We still need to create a unit, because that's when we also
   151  			// connect to the API, but here we use a different service
   152  			// (and hence unit) name.
   153  			createCharm{},
   154  			createServiceAndUnit{serviceName: "w"},
   155  			startUniter{unitTag: "unit-u-0"},
   156  			waitUniterDead{err: `failed to initialize uniter for "unit-u-0": permission denied`},
   157  		),
   158  	})
   159  }
   160  
   161  func (s *UniterSuite) TestUniterBootstrap(c *gc.C) {
   162  	//TODO(bogdanteleaga): Fix this on windows
   163  	if runtime.GOOS == "windows" {
   164  		c.Skip("bug 1403084: currently does not work on windows")
   165  	}
   166  	s.runUniterTests(c, []uniterTest{
   167  		// Check error conditions during unit bootstrap phase.
   168  		ut(
   169  			"insane deployment",
   170  			createCharm{},
   171  			serveCharm{},
   172  			writeFile{"charm", 0644},
   173  			createUniter{},
   174  			waitUniterDead{err: `ModeInstalling cs:quantal/wordpress-0: executing operation "install cs:quantal/wordpress-0": open .*` + errNotDir},
   175  		), ut(
   176  			"charm cannot be downloaded",
   177  			createCharm{},
   178  			// don't serve charm
   179  			createUniter{},
   180  			waitUniterDead{err: `ModeInstalling cs:quantal/wordpress-0: preparing operation "install cs:quantal/wordpress-0": failed to download charm .* 404 Not Found`},
   181  		),
   182  	})
   183  }
   184  
   185  func (s *UniterSuite) TestUniterInstallHook(c *gc.C) {
   186  	s.runUniterTests(c, []uniterTest{
   187  		ut(
   188  			"install hook fail and resolve",
   189  			startupError{"install"},
   190  			verifyWaiting{},
   191  
   192  			resolveError{state.ResolvedNoHooks},
   193  			waitUnitAgent{
   194  				status: params.StatusIdle,
   195  			},
   196  			waitUnitAgent{
   197  				statusGetter: unitStatusGetter,
   198  				status:       params.StatusUnknown,
   199  			},
   200  			waitHooks{"leader-elected", "config-changed", "start"},
   201  		), ut(
   202  			"install hook fail and retry",
   203  			startupError{"install"},
   204  			verifyWaiting{},
   205  
   206  			resolveError{state.ResolvedRetryHooks},
   207  			waitUnitAgent{
   208  				statusGetter: unitStatusGetter,
   209  				status:       params.StatusError,
   210  				info:         `hook failed: "install"`,
   211  				data: map[string]interface{}{
   212  					"hook": "install",
   213  				},
   214  			},
   215  			waitHooks{"fail-install"},
   216  			fixHook{"install"},
   217  			verifyWaiting{},
   218  
   219  			resolveError{state.ResolvedRetryHooks},
   220  			waitUnitAgent{
   221  				status: params.StatusIdle,
   222  			},
   223  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   224  		),
   225  	})
   226  }
   227  
   228  func (s *UniterSuite) TestUniterUpdateStatusHook(c *gc.C) {
   229  	s.runUniterTests(c, []uniterTest{
   230  		ut(
   231  			"update status hook runs on timer",
   232  			createCharm{},
   233  			serveCharm{},
   234  			createUniter{},
   235  			waitHooks(startupHooks(false)),
   236  			waitUnitAgent{status: params.StatusIdle},
   237  			updateStatusHookTick{},
   238  			waitHooks{"update-status"},
   239  		),
   240  	})
   241  }
   242  
   243  func (s *UniterSuite) TestUniterStartHook(c *gc.C) {
   244  	s.runUniterTests(c, []uniterTest{
   245  		ut(
   246  			"start hook fail and resolve",
   247  			startupError{"start"},
   248  			verifyWaiting{},
   249  
   250  			resolveError{state.ResolvedNoHooks},
   251  			waitUnitAgent{
   252  				status: params.StatusIdle,
   253  			},
   254  			waitUnitAgent{
   255  				statusGetter: unitStatusGetter,
   256  				status:       params.StatusMaintenance,
   257  				info:         "installing charm software",
   258  			},
   259  			waitHooks{"config-changed"},
   260  			verifyRunning{},
   261  		), ut(
   262  			"start hook fail and retry",
   263  			startupError{"start"},
   264  			verifyWaiting{},
   265  
   266  			resolveError{state.ResolvedRetryHooks},
   267  			waitUnitAgent{
   268  				statusGetter: unitStatusGetter,
   269  				status:       params.StatusError,
   270  				info:         `hook failed: "start"`,
   271  				data: map[string]interface{}{
   272  					"hook": "start",
   273  				},
   274  			},
   275  			waitHooks{"fail-start"},
   276  			verifyWaiting{},
   277  
   278  			fixHook{"start"},
   279  			resolveError{state.ResolvedRetryHooks},
   280  			waitUnitAgent{
   281  				status: params.StatusIdle,
   282  			},
   283  			waitHooks{"start", "config-changed"},
   284  			verifyRunning{},
   285  		),
   286  	})
   287  }
   288  
   289  func (s *UniterSuite) TestUniterMultipleErrors(c *gc.C) {
   290  	s.runUniterTests(c, []uniterTest{
   291  		ut(
   292  			"resolved is cleared before moving on to next hook",
   293  			createCharm{badHooks: []string{"install", "leader-elected", "config-changed", "start"}},
   294  			serveCharm{},
   295  			createUniter{},
   296  			waitUnitAgent{
   297  				statusGetter: unitStatusGetter,
   298  				status:       params.StatusError,
   299  				info:         `hook failed: "install"`,
   300  				data: map[string]interface{}{
   301  					"hook": "install",
   302  				},
   303  			},
   304  			resolveError{state.ResolvedNoHooks},
   305  			waitUnitAgent{
   306  				statusGetter: unitStatusGetter,
   307  				status:       params.StatusError,
   308  				info:         `hook failed: "leader-elected"`,
   309  				data: map[string]interface{}{
   310  					"hook": "leader-elected",
   311  				},
   312  			},
   313  			resolveError{state.ResolvedNoHooks},
   314  			waitUnitAgent{
   315  				statusGetter: unitStatusGetter,
   316  				status:       params.StatusError,
   317  				info:         `hook failed: "config-changed"`,
   318  				data: map[string]interface{}{
   319  					"hook": "config-changed",
   320  				},
   321  			},
   322  			resolveError{state.ResolvedNoHooks},
   323  			waitUnitAgent{
   324  				statusGetter: unitStatusGetter,
   325  				status:       params.StatusError,
   326  				info:         `hook failed: "start"`,
   327  				data: map[string]interface{}{
   328  					"hook": "start",
   329  				},
   330  			},
   331  		),
   332  	})
   333  }
   334  
   335  func (s *UniterSuite) TestUniterConfigChangedHook(c *gc.C) {
   336  	s.runUniterTests(c, []uniterTest{
   337  		ut(
   338  			"config-changed hook fail and resolve",
   339  			startupError{"config-changed"},
   340  			verifyWaiting{},
   341  
   342  			// Note: we'll run another config-changed as soon as we hit the
   343  			// started state, so the broken hook would actually prevent us
   344  			// from advancing at all if we didn't fix it.
   345  			fixHook{"config-changed"},
   346  			resolveError{state.ResolvedNoHooks},
   347  			waitUnitAgent{
   348  				status: params.StatusIdle,
   349  			},
   350  			waitUnitAgent{
   351  				statusGetter: unitStatusGetter,
   352  				status:       params.StatusUnknown,
   353  			},
   354  			waitHooks{"start", "config-changed"},
   355  			// If we'd accidentally retried that hook, somehow, we would get
   356  			// an extra config-changed as we entered started; see that we don't.
   357  			waitHooks{},
   358  			verifyRunning{},
   359  		), ut(
   360  			"config-changed hook fail and retry",
   361  			startupError{"config-changed"},
   362  			verifyWaiting{},
   363  
   364  			resolveError{state.ResolvedRetryHooks},
   365  			waitUnitAgent{
   366  				statusGetter: unitStatusGetter,
   367  				status:       params.StatusError,
   368  				info:         `hook failed: "config-changed"`,
   369  				data: map[string]interface{}{
   370  					"hook": "config-changed",
   371  				},
   372  			},
   373  			waitHooks{"fail-config-changed"},
   374  			verifyWaiting{},
   375  
   376  			fixHook{"config-changed"},
   377  			resolveError{state.ResolvedRetryHooks},
   378  			waitUnitAgent{
   379  				status: params.StatusIdle,
   380  			},
   381  			waitHooks{"config-changed", "start"},
   382  			verifyRunning{},
   383  		), ut(
   384  			"steady state config change with config-get verification",
   385  			createCharm{
   386  				customize: func(c *gc.C, ctx *context, path string) {
   387  					appendHook(c, path, "config-changed", appendConfigChanged)
   388  				},
   389  			},
   390  			serveCharm{},
   391  			createUniter{},
   392  			waitUnitAgent{
   393  				status: params.StatusIdle,
   394  			},
   395  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   396  			assertYaml{"charm/config.out", map[string]interface{}{
   397  				"blog-title": "My Title",
   398  			}},
   399  			changeConfig{"blog-title": "Goodness Gracious Me"},
   400  			waitHooks{"config-changed"},
   401  			verifyRunning{},
   402  			assertYaml{"charm/config.out", map[string]interface{}{
   403  				"blog-title": "Goodness Gracious Me",
   404  			}},
   405  		),
   406  	})
   407  }
   408  
   409  func (s *UniterSuite) TestUniterHookSynchronisation(c *gc.C) {
   410  	s.runUniterTests(c, []uniterTest{
   411  		ut(
   412  			"verify config change hook not run while lock held",
   413  			quickStart{},
   414  			acquireHookSyncLock{},
   415  			changeConfig{"blog-title": "Goodness Gracious Me"},
   416  			waitHooks{},
   417  			releaseHookSyncLock,
   418  			waitHooks{"config-changed"},
   419  		), ut(
   420  			"verify held lock by this unit is broken",
   421  			acquireHookSyncLock{"u/0:fake"},
   422  			quickStart{},
   423  			verifyHookSyncLockUnlocked,
   424  		), ut(
   425  			"verify held lock by another unit is not broken",
   426  			acquireHookSyncLock{"u/1:fake"},
   427  			// Can't use quickstart as it has a built in waitHooks.
   428  			createCharm{},
   429  			serveCharm{},
   430  			ensureStateWorker{},
   431  			createServiceAndUnit{},
   432  			startUniter{},
   433  			waitAddresses{},
   434  			waitHooks{},
   435  			verifyHookSyncLockLocked,
   436  			releaseHookSyncLock,
   437  			waitUnitAgent{status: params.StatusIdle},
   438  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   439  		),
   440  	})
   441  }
   442  
   443  func (s *UniterSuite) TestUniterDyingReaction(c *gc.C) {
   444  	s.runUniterTests(c, []uniterTest{
   445  		// Reaction to entity deaths.
   446  		ut(
   447  			"steady state service dying",
   448  			quickStart{},
   449  			serviceDying,
   450  			waitHooks{"stop"},
   451  			waitUniterDead{},
   452  		), ut(
   453  			"steady state unit dying",
   454  			quickStart{},
   455  			unitDying,
   456  			waitHooks{"stop"},
   457  			waitUniterDead{},
   458  		), ut(
   459  			"steady state unit dead",
   460  			quickStart{},
   461  			unitDead,
   462  			waitUniterDead{},
   463  			waitHooks{},
   464  		), ut(
   465  			"hook error service dying",
   466  			startupError{"start"},
   467  			serviceDying,
   468  			verifyWaiting{},
   469  			fixHook{"start"},
   470  			resolveError{state.ResolvedRetryHooks},
   471  			waitHooks{"start", "config-changed", "stop"},
   472  			waitUniterDead{},
   473  		), ut(
   474  			"hook error unit dying",
   475  			startupError{"start"},
   476  			unitDying,
   477  			verifyWaiting{},
   478  			fixHook{"start"},
   479  			resolveError{state.ResolvedRetryHooks},
   480  			waitHooks{"start", "config-changed", "stop"},
   481  			waitUniterDead{},
   482  		), ut(
   483  			"hook error unit dead",
   484  			startupError{"start"},
   485  			unitDead,
   486  			waitUniterDead{},
   487  			waitHooks{},
   488  		),
   489  	})
   490  }
   491  
   492  func (s *UniterSuite) TestUniterSteadyStateUpgrade(c *gc.C) {
   493  	s.runUniterTests(c, []uniterTest{
   494  		// Upgrade scenarios from steady state.
   495  		ut(
   496  			"steady state upgrade",
   497  			quickStart{},
   498  			createCharm{revision: 1},
   499  			upgradeCharm{revision: 1},
   500  			waitUnitAgent{
   501  				status: params.StatusIdle,
   502  				charm:  1,
   503  			},
   504  			waitUnitAgent{
   505  				statusGetter: unitStatusGetter,
   506  				status:       params.StatusUnknown,
   507  				charm:        1,
   508  			},
   509  			waitHooks{"upgrade-charm", "config-changed"},
   510  			verifyCharm{revision: 1},
   511  			verifyRunning{},
   512  		), ut(
   513  			"steady state forced upgrade (identical behaviour)",
   514  			quickStart{},
   515  			createCharm{revision: 1},
   516  			upgradeCharm{revision: 1, forced: true},
   517  			waitUnitAgent{
   518  				status: params.StatusIdle,
   519  				charm:  1,
   520  			},
   521  			waitUnitAgent{
   522  				statusGetter: unitStatusGetter,
   523  				status:       params.StatusUnknown,
   524  				charm:        1,
   525  			},
   526  			waitHooks{"upgrade-charm", "config-changed"},
   527  			verifyCharm{revision: 1},
   528  			verifyRunning{},
   529  		), ut(
   530  			"steady state upgrade hook fail and resolve",
   531  			quickStart{},
   532  			createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
   533  			upgradeCharm{revision: 1},
   534  			waitUnitAgent{
   535  				statusGetter: unitStatusGetter,
   536  				status:       params.StatusError,
   537  				info:         `hook failed: "upgrade-charm"`,
   538  				data: map[string]interface{}{
   539  					"hook": "upgrade-charm",
   540  				},
   541  				charm: 1,
   542  			},
   543  			waitHooks{"fail-upgrade-charm"},
   544  			verifyCharm{revision: 1},
   545  			verifyWaiting{},
   546  
   547  			resolveError{state.ResolvedNoHooks},
   548  			waitUnitAgent{
   549  				status: params.StatusIdle,
   550  				charm:  1,
   551  			},
   552  			waitUnitAgent{
   553  				statusGetter: unitStatusGetter,
   554  				status:       params.StatusUnknown,
   555  				charm:        1,
   556  			},
   557  			waitHooks{"config-changed"},
   558  			verifyRunning{},
   559  		), ut(
   560  			"steady state upgrade hook fail and retry",
   561  			quickStart{},
   562  			createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
   563  			upgradeCharm{revision: 1},
   564  			waitUnitAgent{
   565  				statusGetter: unitStatusGetter,
   566  				status:       params.StatusError,
   567  				info:         `hook failed: "upgrade-charm"`,
   568  				data: map[string]interface{}{
   569  					"hook": "upgrade-charm",
   570  				},
   571  				charm: 1,
   572  			},
   573  			waitHooks{"fail-upgrade-charm"},
   574  			verifyCharm{revision: 1},
   575  			verifyWaiting{},
   576  
   577  			resolveError{state.ResolvedRetryHooks},
   578  			waitUnitAgent{
   579  				statusGetter: unitStatusGetter,
   580  				status:       params.StatusError,
   581  				info:         `hook failed: "upgrade-charm"`,
   582  				data: map[string]interface{}{
   583  					"hook": "upgrade-charm",
   584  				},
   585  				charm: 1,
   586  			},
   587  			waitHooks{"fail-upgrade-charm"},
   588  			verifyWaiting{},
   589  
   590  			fixHook{"upgrade-charm"},
   591  			resolveError{state.ResolvedRetryHooks},
   592  			waitUnitAgent{
   593  				status: params.StatusIdle,
   594  				charm:  1,
   595  			},
   596  			waitHooks{"upgrade-charm", "config-changed"},
   597  			verifyRunning{},
   598  		), ut(
   599  			// This test does an add-relation as quickly as possible
   600  			// after an upgrade-charm, in the hope that the scheduler will
   601  			// deliver the events in the wrong order. The observed
   602  			// behaviour should be the same in either case.
   603  			"ignore unknown relations until upgrade is done",
   604  			quickStart{},
   605  			createCharm{
   606  				revision: 2,
   607  				customize: func(c *gc.C, ctx *context, path string) {
   608  					renameRelation(c, path, "db", "db2")
   609  					hpath := filepath.Join(path, "hooks", "db2-relation-joined")
   610  					ctx.writeHook(c, hpath, true)
   611  				},
   612  			},
   613  			serveCharm{},
   614  			upgradeCharm{revision: 2},
   615  			addRelation{},
   616  			addRelationUnit{},
   617  			waitHooks{"upgrade-charm", "config-changed", "db2-relation-joined mysql/0 db2:0"},
   618  			verifyCharm{revision: 2},
   619  		),
   620  	})
   621  }
   622  
   623  func (s *UniterSuite) TestUniterUpgradeOverwrite(c *gc.C) {
   624  	//TODO(bogdanteleaga): Fix this on windows
   625  	if runtime.GOOS == "windows" {
   626  		c.Skip("bug 1403084: currently does not work on windows")
   627  	}
   628  	makeTest := func(description string, content, extraChecks ft.Entries) uniterTest {
   629  		return ut(description,
   630  			createCharm{
   631  				// This is the base charm which all upgrade tests start out running.
   632  				customize: func(c *gc.C, ctx *context, path string) {
   633  					ft.Entries{
   634  						ft.Dir{"dir", 0755},
   635  						ft.File{"file", "blah", 0644},
   636  						ft.Symlink{"symlink", "file"},
   637  					}.Create(c, path)
   638  					// Note that it creates "dir/user-file" at runtime, which may be
   639  					// preserved or removed depending on the test.
   640  					script := "echo content > dir/user-file && chmod 755 dir/user-file"
   641  					appendHook(c, path, "start", script)
   642  				},
   643  			},
   644  			serveCharm{},
   645  			createUniter{},
   646  			waitUnitAgent{
   647  				status: params.StatusIdle,
   648  			},
   649  			waitUnitAgent{
   650  				statusGetter: unitStatusGetter,
   651  				status:       params.StatusUnknown,
   652  			},
   653  			waitHooks{"install", "leader-elected", "config-changed", "start"},
   654  
   655  			createCharm{
   656  				revision: 1,
   657  				customize: func(c *gc.C, _ *context, path string) {
   658  					content.Create(c, path)
   659  				},
   660  			},
   661  			serveCharm{},
   662  			upgradeCharm{revision: 1},
   663  			waitUnitAgent{
   664  				status: params.StatusIdle,
   665  				charm:  1,
   666  			},
   667  			waitUnitAgent{
   668  				statusGetter: unitStatusGetter,
   669  				status:       params.StatusUnknown,
   670  				charm:        1,
   671  			},
   672  			waitHooks{"upgrade-charm", "config-changed"},
   673  			verifyCharm{revision: 1},
   674  			custom{func(c *gc.C, ctx *context) {
   675  				path := filepath.Join(ctx.path, "charm")
   676  				content.Check(c, path)
   677  				extraChecks.Check(c, path)
   678  			}},
   679  			verifyRunning{},
   680  		)
   681  	}
   682  
   683  	s.runUniterTests(c, []uniterTest{
   684  		makeTest(
   685  			"files overwite files, dirs, symlinks",
   686  			ft.Entries{
   687  				ft.File{"file", "new", 0755},
   688  				ft.File{"dir", "new", 0755},
   689  				ft.File{"symlink", "new", 0755},
   690  			},
   691  			ft.Entries{
   692  				ft.Removed{"dir/user-file"},
   693  			},
   694  		), makeTest(
   695  			"symlinks overwite files, dirs, symlinks",
   696  			ft.Entries{
   697  				ft.Symlink{"file", "new"},
   698  				ft.Symlink{"dir", "new"},
   699  				ft.Symlink{"symlink", "new"},
   700  			},
   701  			ft.Entries{
   702  				ft.Removed{"dir/user-file"},
   703  			},
   704  		), makeTest(
   705  			"dirs overwite files, symlinks; merge dirs",
   706  			ft.Entries{
   707  				ft.Dir{"file", 0755},
   708  				ft.Dir{"dir", 0755},
   709  				ft.File{"dir/charm-file", "charm-content", 0644},
   710  				ft.Dir{"symlink", 0755},
   711  			},
   712  			ft.Entries{
   713  				ft.File{"dir/user-file", "content\n", 0755},
   714  			},
   715  		),
   716  	})
   717  }
   718  
   719  func (s *UniterSuite) TestUniterErrorStateUpgrade(c *gc.C) {
   720  	s.runUniterTests(c, []uniterTest{
   721  		// Upgrade scenarios from error state.
   722  		ut(
   723  			"error state unforced upgrade (ignored until started state)",
   724  			startupError{"start"},
   725  			createCharm{revision: 1},
   726  			upgradeCharm{revision: 1},
   727  			waitUnitAgent{
   728  				statusGetter: unitStatusGetter,
   729  				status:       params.StatusError,
   730  				info:         `hook failed: "start"`,
   731  				data: map[string]interface{}{
   732  					"hook": "start",
   733  				},
   734  			},
   735  			waitHooks{},
   736  			verifyCharm{},
   737  			verifyWaiting{},
   738  
   739  			resolveError{state.ResolvedNoHooks},
   740  			waitUnitAgent{
   741  				status: params.StatusIdle,
   742  				charm:  1,
   743  			},
   744  			waitUnitAgent{
   745  				statusGetter: unitStatusGetter,
   746  				status:       params.StatusMaintenance,
   747  				info:         "installing charm software",
   748  				charm:        1,
   749  			},
   750  			waitHooks{"config-changed", "upgrade-charm", "config-changed"},
   751  			verifyCharm{revision: 1},
   752  			verifyRunning{},
   753  		), ut(
   754  			"error state forced upgrade",
   755  			startupError{"start"},
   756  			createCharm{revision: 1},
   757  			upgradeCharm{revision: 1, forced: true},
   758  			// It's not possible to tell directly from state when the upgrade is
   759  			// complete, because the new unit charm URL is set at the upgrade
   760  			// process's point of no return (before actually deploying, but after
   761  			// the charm has been downloaded and verified). However, it's still
   762  			// useful to wait until that point...
   763  			waitUnitAgent{
   764  				statusGetter: unitStatusGetter,
   765  				status:       params.StatusError,
   766  				info:         `hook failed: "start"`,
   767  				data: map[string]interface{}{
   768  					"hook": "start",
   769  				},
   770  				charm: 1,
   771  			},
   772  			// ...because the uniter *will* complete a started deployment even if
   773  			// we stop it from outside. So, by stopping and starting, we can be
   774  			// sure that the operation has completed and can safely verify that
   775  			// the charm state on disk is as we expect.
   776  			verifyWaiting{},
   777  			verifyCharm{revision: 1},
   778  
   779  			resolveError{state.ResolvedNoHooks},
   780  			waitUnitAgent{
   781  				status: params.StatusIdle,
   782  				charm:  1,
   783  			},
   784  			waitHooks{"config-changed"},
   785  			verifyRunning{},
   786  		),
   787  	})
   788  }
   789  
   790  func (s *UniterSuite) TestUniterDeployerConversion(c *gc.C) {
   791  	coretesting.SkipIfGitNotAvailable(c)
   792  
   793  	deployerConversionTests := []uniterTest{
   794  		ut(
   795  			"install normally, check not using git",
   796  			quickStart{},
   797  			verifyCharm{
   798  				checkFiles: ft.Entries{ft.Removed{".git"}},
   799  			},
   800  		), ut(
   801  			"install with git, restart in steady state",
   802  			prepareGitUniter{[]stepper{
   803  				quickStart{},
   804  				verifyGitCharm{},
   805  				stopUniter{},
   806  			}},
   807  			startUniter{},
   808  			waitHooks{"config-changed"},
   809  
   810  			// At this point, the deployer has been converted, but the
   811  			// charm directory itself hasn't; the *next* deployment will
   812  			// actually hit the charm directory and strip out the git
   813  			// stuff.
   814  			createCharm{revision: 1},
   815  			upgradeCharm{revision: 1},
   816  			waitHooks{"upgrade-charm", "config-changed"},
   817  			waitUnitAgent{
   818  				status: params.StatusIdle,
   819  				charm:  1,
   820  			},
   821  			waitUnitAgent{
   822  				statusGetter: unitStatusGetter,
   823  				status:       params.StatusUnknown,
   824  				charm:        1,
   825  			},
   826  			verifyCharm{
   827  				revision:   1,
   828  				checkFiles: ft.Entries{ft.Removed{".git"}},
   829  			},
   830  			verifyRunning{},
   831  		), ut(
   832  			"install with git, get conflicted, mark resolved",
   833  			prepareGitUniter{[]stepper{
   834  				startGitUpgradeError{},
   835  				stopUniter{},
   836  			}},
   837  			startUniter{},
   838  
   839  			resolveError{state.ResolvedNoHooks},
   840  			waitHooks{"upgrade-charm", "config-changed"},
   841  			waitUnitAgent{
   842  				status: params.StatusIdle,
   843  				charm:  1,
   844  			},
   845  			waitUnitAgent{
   846  				statusGetter: unitStatusGetter,
   847  				status:       params.StatusUnknown,
   848  				charm:        1,
   849  			},
   850  			verifyCharm{revision: 1},
   851  			verifyRunning{},
   852  
   853  			// Due to the uncertainties around marking upgrade conflicts resolved,
   854  			// the charm directory again remains unconverted, although the deployer
   855  			// should have been fixed. Again, we check this by running another
   856  			// upgrade and verifying the .git dir is then removed.
   857  			createCharm{revision: 2},
   858  			upgradeCharm{revision: 2},
   859  			waitHooks{"upgrade-charm", "config-changed"},
   860  			waitUnitAgent{
   861  				status: params.StatusIdle,
   862  				charm:  2,
   863  			},
   864  			waitUnitAgent{
   865  				statusGetter: unitStatusGetter,
   866  				status:       params.StatusUnknown,
   867  				charm:        2,
   868  			},
   869  			verifyCharm{
   870  				revision:   2,
   871  				checkFiles: ft.Entries{ft.Removed{".git"}},
   872  			},
   873  			verifyRunning{},
   874  		), ut(
   875  			"install with git, get conflicted, force an upgrade",
   876  			prepareGitUniter{[]stepper{
   877  				startGitUpgradeError{},
   878  				stopUniter{},
   879  			}},
   880  			startUniter{},
   881  
   882  			createCharm{
   883  				revision: 2,
   884  				customize: func(c *gc.C, ctx *context, path string) {
   885  					ft.File{"data", "OVERWRITE!", 0644}.Create(c, path)
   886  				},
   887  			},
   888  			serveCharm{},
   889  			upgradeCharm{revision: 2, forced: true},
   890  			waitHooks{"upgrade-charm", "config-changed"},
   891  			waitUnitAgent{
   892  				status: params.StatusIdle,
   893  				charm:  2,
   894  			},
   895  
   896  			// A forced upgrade allows us to swap out the git deployer *and*
   897  			// the .git dir inside the charm immediately; check we did so.
   898  			verifyCharm{
   899  				revision: 2,
   900  				checkFiles: ft.Entries{
   901  					ft.Removed{".git"},
   902  					ft.File{"data", "OVERWRITE!", 0644},
   903  				},
   904  			},
   905  			verifyRunning{},
   906  		),
   907  	}
   908  	s.runUniterTests(c, deployerConversionTests)
   909  }
   910  
   911  func (s *UniterSuite) TestUniterUpgradeConflicts(c *gc.C) {
   912  	coretesting.SkipIfPPC64EL(c, "lp:1448308")
   913  	//TODO(bogdanteleaga): Fix this on windows
   914  	if runtime.GOOS == "windows" {
   915  		c.Skip("bug 1403084: currently does not work on windows")
   916  	}
   917  	s.runUniterTests(c, []uniterTest{
   918  		// Upgrade scenarios - handling conflicts.
   919  		ut(
   920  			"upgrade: resolving doesn't help until underlying problem is fixed",
   921  			startUpgradeError{},
   922  			resolveError{state.ResolvedNoHooks},
   923  			verifyWaitingUpgradeError{revision: 1},
   924  			fixUpgradeError{},
   925  			resolveError{state.ResolvedNoHooks},
   926  			waitHooks{"upgrade-charm", "config-changed"},
   927  			waitUnitAgent{
   928  				status: params.StatusIdle,
   929  				charm:  1,
   930  			},
   931  			waitUnitAgent{
   932  				statusGetter: unitStatusGetter,
   933  				status:       params.StatusUnknown,
   934  				charm:        1,
   935  			},
   936  			verifyCharm{revision: 1},
   937  		), ut(
   938  			`upgrade: forced upgrade does work without explicit resolution if underlying problem was fixed`,
   939  			startUpgradeError{},
   940  			resolveError{state.ResolvedNoHooks},
   941  			verifyWaitingUpgradeError{revision: 1},
   942  			fixUpgradeError{},
   943  			createCharm{revision: 2},
   944  			serveCharm{},
   945  			upgradeCharm{revision: 2, forced: true},
   946  			waitHooks{"upgrade-charm", "config-changed"},
   947  			waitUnitAgent{
   948  				status: params.StatusIdle,
   949  				charm:  2,
   950  			},
   951  			waitUnitAgent{
   952  				statusGetter: unitStatusGetter,
   953  				status:       params.StatusUnknown,
   954  				charm:        2,
   955  			},
   956  			verifyCharm{revision: 2},
   957  		), ut(
   958  			"upgrade conflict service dying",
   959  			startUpgradeError{},
   960  			serviceDying,
   961  			verifyWaitingUpgradeError{revision: 1},
   962  			fixUpgradeError{},
   963  			resolveError{state.ResolvedNoHooks},
   964  			waitHooks{"upgrade-charm", "config-changed", "stop"},
   965  			waitUniterDead{},
   966  		), ut(
   967  			"upgrade conflict unit dying",
   968  			startUpgradeError{},
   969  			unitDying,
   970  			verifyWaitingUpgradeError{revision: 1},
   971  			fixUpgradeError{},
   972  			resolveError{state.ResolvedNoHooks},
   973  			waitHooks{"upgrade-charm", "config-changed", "stop"},
   974  			waitUniterDead{},
   975  		), ut(
   976  			"upgrade conflict unit dead",
   977  			startUpgradeError{},
   978  			unitDead,
   979  			waitUniterDead{},
   980  			waitHooks{},
   981  			fixUpgradeError{},
   982  		),
   983  	})
   984  }
   985  
   986  func (s *UniterSuite) TestUniterUpgradeGitConflicts(c *gc.C) {
   987  	coretesting.SkipIfGitNotAvailable(c)
   988  
   989  	// These tests are copies of the old git-deployer-related tests, to test that
   990  	// the uniter with the manifest-deployer work patched out still works how it
   991  	// used to; thus demonstrating that the *other* tests that verify manifest
   992  	// deployer behaviour in the presence of an old git deployer are working against
   993  	// an accurate representation of the base state.
   994  	// The only actual behaviour change is that we no longer commit changes after
   995  	// each hook execution; this is reflected by checking that it's dirty in a couple
   996  	// of places where we once checked it was not.
   997  
   998  	s.runUniterTests(c, []uniterTest{
   999  		// Upgrade scenarios - handling conflicts.
  1000  		ugt(
  1001  			"upgrade: conflicting files",
  1002  			startGitUpgradeError{},
  1003  
  1004  			// NOTE: this is just dumbly committing the conflicts, but AFAICT this
  1005  			// is the only reasonable solution; if the user tells us it's resolved
  1006  			// we have to take their word for it.
  1007  			resolveError{state.ResolvedNoHooks},
  1008  			waitHooks{"upgrade-charm", "config-changed"},
  1009  			waitUnitAgent{
  1010  				status: params.StatusIdle,
  1011  				charm:  1,
  1012  			},
  1013  			waitUnitAgent{
  1014  				statusGetter: unitStatusGetter,
  1015  				status:       params.StatusUnknown,
  1016  				charm:        1,
  1017  			},
  1018  			verifyGitCharm{revision: 1},
  1019  		), ugt(
  1020  			`upgrade: conflicting directories`,
  1021  			createCharm{
  1022  				customize: func(c *gc.C, ctx *context, path string) {
  1023  					err := os.Mkdir(filepath.Join(path, "data"), 0755)
  1024  					c.Assert(err, jc.ErrorIsNil)
  1025  					appendHook(c, path, "start", "echo DATA > data/newfile")
  1026  				},
  1027  			},
  1028  			serveCharm{},
  1029  			createUniter{},
  1030  			waitUnitAgent{
  1031  				status: params.StatusIdle,
  1032  			},
  1033  			waitUnitAgent{
  1034  				statusGetter: unitStatusGetter,
  1035  				status:       params.StatusUnknown,
  1036  			},
  1037  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1038  			verifyGitCharm{dirty: true},
  1039  
  1040  			createCharm{
  1041  				revision: 1,
  1042  				customize: func(c *gc.C, ctx *context, path string) {
  1043  					data := filepath.Join(path, "data")
  1044  					err := ioutil.WriteFile(data, []byte("<nelson>ha ha</nelson>"), 0644)
  1045  					c.Assert(err, jc.ErrorIsNil)
  1046  				},
  1047  			},
  1048  			serveCharm{},
  1049  			upgradeCharm{revision: 1},
  1050  			waitUnitAgent{
  1051  				statusGetter: unitStatusGetter,
  1052  				status:       params.StatusError,
  1053  				info:         "upgrade failed",
  1054  				charm:        1,
  1055  			},
  1056  			verifyWaiting{},
  1057  			verifyGitCharm{dirty: true},
  1058  
  1059  			resolveError{state.ResolvedNoHooks},
  1060  			waitHooks{"upgrade-charm", "config-changed"},
  1061  			waitUnitAgent{
  1062  				status: params.StatusIdle,
  1063  				charm:  1,
  1064  			},
  1065  			verifyGitCharm{revision: 1},
  1066  		), ugt(
  1067  			"upgrade conflict resolved with forced upgrade",
  1068  			startGitUpgradeError{},
  1069  			createCharm{
  1070  				revision: 2,
  1071  				customize: func(c *gc.C, ctx *context, path string) {
  1072  					otherdata := filepath.Join(path, "otherdata")
  1073  					err := ioutil.WriteFile(otherdata, []byte("blah"), 0644)
  1074  					c.Assert(err, jc.ErrorIsNil)
  1075  				},
  1076  			},
  1077  			serveCharm{},
  1078  			upgradeCharm{revision: 2, forced: true},
  1079  			waitUnitAgent{
  1080  				status: params.StatusIdle,
  1081  				charm:  2,
  1082  			}, waitUnitAgent{
  1083  				statusGetter: unitStatusGetter,
  1084  				status:       params.StatusUnknown,
  1085  				charm:        2,
  1086  			},
  1087  			waitHooks{"upgrade-charm", "config-changed"},
  1088  			verifyGitCharm{revision: 2},
  1089  			custom{func(c *gc.C, ctx *context) {
  1090  				// otherdata should exist (in v2)
  1091  				otherdata, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "otherdata"))
  1092  				c.Assert(err, jc.ErrorIsNil)
  1093  				c.Assert(string(otherdata), gc.Equals, "blah")
  1094  
  1095  				// ignore should not (only in v1)
  1096  				_, err = os.Stat(filepath.Join(ctx.path, "charm", "ignore"))
  1097  				c.Assert(err, jc.Satisfies, os.IsNotExist)
  1098  
  1099  				// data should contain what was written in the start hook
  1100  				data, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "data"))
  1101  				c.Assert(err, jc.ErrorIsNil)
  1102  				c.Assert(string(data), gc.Equals, "STARTDATA\n")
  1103  			}},
  1104  		), ugt(
  1105  			"upgrade conflict service dying",
  1106  			startGitUpgradeError{},
  1107  			serviceDying,
  1108  			verifyWaiting{},
  1109  			resolveError{state.ResolvedNoHooks},
  1110  			waitHooks{"upgrade-charm", "config-changed", "stop"},
  1111  			waitUniterDead{},
  1112  		), ugt(
  1113  			"upgrade conflict unit dying",
  1114  			startGitUpgradeError{},
  1115  			unitDying,
  1116  			verifyWaiting{},
  1117  			resolveError{state.ResolvedNoHooks},
  1118  			waitHooks{"upgrade-charm", "config-changed", "stop"},
  1119  			waitUniterDead{},
  1120  		), ugt(
  1121  			"upgrade conflict unit dead",
  1122  			startGitUpgradeError{},
  1123  			unitDead,
  1124  			waitUniterDead{},
  1125  			waitHooks{},
  1126  		),
  1127  	})
  1128  }
  1129  
  1130  func (s *UniterSuite) TestUniterRelations(c *gc.C) {
  1131  	waitDyingHooks := custom{func(c *gc.C, ctx *context) {
  1132  		// There is no ordering relationship between relation hooks and
  1133  		// leader-settings-changed hooks; and while we're dying we may
  1134  		// never get to leader-settings-changed before it's time to run
  1135  		// the stop (as we might not react to a config change in time).
  1136  		// It's actually clearer to just list the possible orders:
  1137  		possibles := [][]string{{
  1138  			"leader-settings-changed",
  1139  			"db-relation-departed mysql/0 db:0",
  1140  			"db-relation-broken db:0",
  1141  			"stop",
  1142  		}, {
  1143  			"db-relation-departed mysql/0 db:0",
  1144  			"leader-settings-changed",
  1145  			"db-relation-broken db:0",
  1146  			"stop",
  1147  		}, {
  1148  			"db-relation-departed mysql/0 db:0",
  1149  			"db-relation-broken db:0",
  1150  			"leader-settings-changed",
  1151  			"stop",
  1152  		}, {
  1153  			"db-relation-departed mysql/0 db:0",
  1154  			"db-relation-broken db:0",
  1155  			"stop",
  1156  		}}
  1157  		unchecked := ctx.hooksCompleted[len(ctx.hooks):]
  1158  		for _, possible := range possibles {
  1159  			if ok, _ := jc.DeepEqual(unchecked, possible); ok {
  1160  				return
  1161  			}
  1162  		}
  1163  		c.Fatalf("unexpected hooks: %v", unchecked)
  1164  	}}
  1165  	s.runUniterTests(c, []uniterTest{
  1166  		// Relations.
  1167  		ut(
  1168  			"simple joined/changed/departed",
  1169  			quickStartRelation{},
  1170  			addRelationUnit{},
  1171  			waitHooks{
  1172  				"db-relation-joined mysql/1 db:0",
  1173  				"db-relation-changed mysql/1 db:0",
  1174  			},
  1175  			changeRelationUnit{"mysql/0"},
  1176  			waitHooks{"db-relation-changed mysql/0 db:0"},
  1177  			removeRelationUnit{"mysql/1"},
  1178  			waitHooks{"db-relation-departed mysql/1 db:0"},
  1179  			verifyRunning{},
  1180  		), ut(
  1181  			"relation becomes dying; unit is not last remaining member",
  1182  			quickStartRelation{},
  1183  			relationDying,
  1184  			waitHooks{
  1185  				"db-relation-departed mysql/0 db:0",
  1186  				"db-relation-broken db:0",
  1187  			},
  1188  			verifyRunning{},
  1189  			relationState{life: state.Dying},
  1190  			removeRelationUnit{"mysql/0"},
  1191  			verifyRunning{},
  1192  			relationState{removed: true},
  1193  			verifyRunning{},
  1194  		), ut(
  1195  			"relation becomes dying; unit is last remaining member",
  1196  			quickStartRelation{},
  1197  			removeRelationUnit{"mysql/0"},
  1198  			waitHooks{"db-relation-departed mysql/0 db:0"},
  1199  			relationDying,
  1200  			waitHooks{"db-relation-broken db:0"},
  1201  			verifyRunning{},
  1202  			relationState{removed: true},
  1203  			verifyRunning{},
  1204  		), ut(
  1205  			"service becomes dying while in a relation",
  1206  			quickStartRelation{},
  1207  			serviceDying,
  1208  			waitUniterDead{},
  1209  			waitDyingHooks,
  1210  			relationState{life: state.Dying},
  1211  			removeRelationUnit{"mysql/0"},
  1212  			relationState{removed: true},
  1213  		), ut(
  1214  			"unit becomes dying while in a relation",
  1215  			quickStartRelation{},
  1216  			unitDying,
  1217  			waitUniterDead{},
  1218  			waitDyingHooks,
  1219  			relationState{life: state.Alive},
  1220  			removeRelationUnit{"mysql/0"},
  1221  			relationState{life: state.Alive},
  1222  		), ut(
  1223  			"unit becomes dead while in a relation",
  1224  			quickStartRelation{},
  1225  			unitDead,
  1226  			waitUniterDead{},
  1227  			waitHooks{},
  1228  			// TODO BUG(?): the unit doesn't leave the scope, leaving the relation
  1229  			// unkillable without direct intervention. I'm pretty sure it's not a
  1230  			// uniter bug -- it should be the responsibility of `juju remove-unit
  1231  			// --force` to cause the unit to leave any relation scopes it may be
  1232  			// in -- but it's worth noting here all the same.
  1233  		), ut(
  1234  			"unknown local relation dir is removed",
  1235  			quickStartRelation{},
  1236  			stopUniter{},
  1237  			custom{func(c *gc.C, ctx *context) {
  1238  				ft.Dir{"state/relations/90210", 0755}.Create(c, ctx.path)
  1239  			}},
  1240  			startUniter{},
  1241  			waitHooks{"config-changed"},
  1242  			custom{func(c *gc.C, ctx *context) {
  1243  				ft.Removed{"state/relations/90210"}.Check(c, ctx.path)
  1244  			}},
  1245  		), ut(
  1246  			"all relations are available to config-changed on bounce, even if state dir is missing",
  1247  			createCharm{
  1248  				customize: func(c *gc.C, ctx *context, path string) {
  1249  					script := uniterRelationsCustomizeScript
  1250  					appendHook(c, path, "config-changed", script)
  1251  				},
  1252  			},
  1253  			serveCharm{},
  1254  			createUniter{},
  1255  			waitUnitAgent{
  1256  				status: params.StatusIdle,
  1257  			},
  1258  			waitUnitAgent{
  1259  				statusGetter: unitStatusGetter,
  1260  				status:       params.StatusUnknown,
  1261  			},
  1262  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1263  			addRelation{waitJoin: true},
  1264  			stopUniter{},
  1265  			custom{func(c *gc.C, ctx *context) {
  1266  				// Check the state dir was created, and remove it.
  1267  				path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
  1268  				ft.Dir{path, 0755}.Check(c, ctx.path)
  1269  				ft.Removed{path}.Create(c, ctx.path)
  1270  
  1271  				// Check that config-changed didn't record any relations, because
  1272  				// they shouldn't been available until after the start hook.
  1273  				ft.File{"charm/relations.out", "", 0644}.Check(c, ctx.path)
  1274  			}},
  1275  			startUniter{},
  1276  			waitHooks{"config-changed"},
  1277  			custom{func(c *gc.C, ctx *context) {
  1278  				// Check the state dir was recreated.
  1279  				path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
  1280  				ft.Dir{path, 0755}.Check(c, ctx.path)
  1281  
  1282  				// Check that config-changed did record the joined relations.
  1283  				data := fmt.Sprintf("db:%d\n", ctx.relation.Id())
  1284  				ft.File{"charm/relations.out", data, 0644}.Check(c, ctx.path)
  1285  			}},
  1286  		),
  1287  	})
  1288  }
  1289  
  1290  func (s *UniterSuite) TestUniterRelationErrors(c *gc.C) {
  1291  	s.runUniterTests(c, []uniterTest{
  1292  		ut(
  1293  			"hook error during join of a relation",
  1294  			startupRelationError{"db-relation-joined"},
  1295  			waitUnitAgent{
  1296  				statusGetter: unitStatusGetter,
  1297  				status:       params.StatusError,
  1298  				info:         `hook failed: "db-relation-joined"`,
  1299  				data: map[string]interface{}{
  1300  					"hook":        "db-relation-joined",
  1301  					"relation-id": 0,
  1302  					"remote-unit": "mysql/0",
  1303  				},
  1304  			},
  1305  		), ut(
  1306  			"hook error during change of a relation",
  1307  			startupRelationError{"db-relation-changed"},
  1308  			waitUnitAgent{
  1309  				statusGetter: unitStatusGetter,
  1310  				status:       params.StatusError,
  1311  				info:         `hook failed: "db-relation-changed"`,
  1312  				data: map[string]interface{}{
  1313  					"hook":        "db-relation-changed",
  1314  					"relation-id": 0,
  1315  					"remote-unit": "mysql/0",
  1316  				},
  1317  			},
  1318  		), ut(
  1319  			"hook error after a unit departed",
  1320  			startupRelationError{"db-relation-departed"},
  1321  			waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
  1322  			removeRelationUnit{"mysql/0"},
  1323  			waitUnitAgent{
  1324  				statusGetter: unitStatusGetter,
  1325  				status:       params.StatusError,
  1326  				info:         `hook failed: "db-relation-departed"`,
  1327  				data: map[string]interface{}{
  1328  					"hook":        "db-relation-departed",
  1329  					"relation-id": 0,
  1330  					"remote-unit": "mysql/0",
  1331  				},
  1332  			},
  1333  		),
  1334  		ut(
  1335  			"hook error after a relation died",
  1336  			startupRelationError{"db-relation-broken"},
  1337  			waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
  1338  			relationDying,
  1339  			waitUnitAgent{
  1340  				statusGetter: unitStatusGetter,
  1341  				status:       params.StatusError,
  1342  				info:         `hook failed: "db-relation-broken"`,
  1343  				data: map[string]interface{}{
  1344  					"hook":        "db-relation-broken",
  1345  					"relation-id": 0,
  1346  				},
  1347  			},
  1348  		),
  1349  	})
  1350  }
  1351  
  1352  func (s *UniterSuite) TestUniterMeterStatusChanged(c *gc.C) {
  1353  	s.runUniterTests(c, []uniterTest{
  1354  		ut(
  1355  			"meter status event triggered by unit meter status change",
  1356  			quickStart{},
  1357  			changeMeterStatus{"AMBER", "Investigate charm."},
  1358  			waitHooks{"meter-status-changed"},
  1359  		),
  1360  	})
  1361  }
  1362  
  1363  func (s *UniterSuite) TestUniterSendMetricsBeforeDying(c *gc.C) {
  1364  	s.runUniterTests(c, []uniterTest{
  1365  		ut(
  1366  			"metrics must be sent before the unit is destroyed",
  1367  			createCharm{
  1368  				customize: func(c *gc.C, ctx *context, path string) {
  1369  					ctx.writeMetricsYaml(c, path)
  1370  				},
  1371  			},
  1372  			serveCharm{},
  1373  			createUniter{},
  1374  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1375  			addMetrics{[]string{"5", "42"}},
  1376  			unitDying,
  1377  			waitUniterDead{},
  1378  			checkStateMetrics{number: 1, values: []string{"5", "42"}},
  1379  		),
  1380  	})
  1381  }
  1382  
  1383  func (s *UniterSuite) TestUniterCollectMetrics(c *gc.C) {
  1384  	s.runUniterTests(c, []uniterTest{
  1385  		ut(
  1386  			"collect-metrics event triggered by manual timer",
  1387  			createCharm{
  1388  				customize: func(c *gc.C, ctx *context, path string) {
  1389  					ctx.writeMetricsYaml(c, path)
  1390  				},
  1391  			},
  1392  			serveCharm{},
  1393  			createUniter{},
  1394  			waitUnitAgent{status: params.StatusIdle},
  1395  			waitUnitAgent{
  1396  				statusGetter: unitStatusGetter,
  1397  				status:       params.StatusUnknown,
  1398  			},
  1399  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1400  			verifyCharm{},
  1401  			collectMetricsTick{},
  1402  			waitHooks{"collect-metrics"},
  1403  		), ut(
  1404  			"collect-metrics resumed after hook error",
  1405  			startupErrorWithCustomCharm{
  1406  				badHook: "config-changed",
  1407  				customize: func(c *gc.C, ctx *context, path string) {
  1408  					ctx.writeMetricsYaml(c, path)
  1409  				},
  1410  			},
  1411  			collectMetricsTick{expectFail: true},
  1412  			fixHook{"config-changed"},
  1413  			resolveError{state.ResolvedRetryHooks},
  1414  			waitUnitAgent{
  1415  				status: params.StatusIdle,
  1416  			},
  1417  			waitUnitAgent{
  1418  				statusGetter: unitStatusGetter,
  1419  				status:       params.StatusUnknown,
  1420  			},
  1421  			waitHooks{"config-changed", "start"},
  1422  			collectMetricsTick{},
  1423  			waitHooks{"collect-metrics"},
  1424  			verifyRunning{},
  1425  		),
  1426  		ut(
  1427  			"collect-metrics state maintained during uniter restart",
  1428  			startupErrorWithCustomCharm{
  1429  				badHook: "config-changed",
  1430  				customize: func(c *gc.C, ctx *context, path string) {
  1431  					ctx.writeMetricsYaml(c, path)
  1432  				},
  1433  			},
  1434  			collectMetricsTick{expectFail: true},
  1435  			fixHook{"config-changed"},
  1436  			stopUniter{},
  1437  			startUniter{},
  1438  			resolveError{state.ResolvedRetryHooks},
  1439  			waitUnitAgent{
  1440  				status: params.StatusIdle,
  1441  			},
  1442  			waitUnitAgent{
  1443  				statusGetter: unitStatusGetter,
  1444  				status:       params.StatusUnknown,
  1445  			},
  1446  			waitHooks{"config-changed", "start"},
  1447  			collectMetricsTick{},
  1448  			waitHooks{"collect-metrics"},
  1449  			verifyRunning{},
  1450  		), ut(
  1451  			"collect-metrics event not triggered for non-metered charm",
  1452  			quickStart{},
  1453  			collectMetricsTick{expectFail: true},
  1454  			waitHooks{},
  1455  		),
  1456  	})
  1457  }
  1458  
  1459  func (s *UniterSuite) TestUniterSendMetrics(c *gc.C) {
  1460  	s.runUniterTests(c, []uniterTest{
  1461  		ut(
  1462  			"send metrics event triggered by manual timer",
  1463  			createCharm{
  1464  				customize: func(c *gc.C, ctx *context, path string) {
  1465  					ctx.writeMetricsYaml(c, path)
  1466  				},
  1467  			},
  1468  			serveCharm{},
  1469  			createUniter{},
  1470  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1471  			addMetrics{[]string{"15", "17"}},
  1472  			sendMetricsTick{},
  1473  			checkStateMetrics{number: 1, values: []string{"17", "15"}},
  1474  		), ut(
  1475  			"send-metrics resumed after hook error",
  1476  			startupErrorWithCustomCharm{
  1477  				badHook: "config-changed",
  1478  				customize: func(c *gc.C, ctx *context, path string) {
  1479  					ctx.writeMetricsYaml(c, path)
  1480  				},
  1481  			},
  1482  			addMetrics{[]string{"15"}},
  1483  			sendMetricsTick{expectFail: true},
  1484  			fixHook{"config-changed"},
  1485  			resolveError{state.ResolvedRetryHooks},
  1486  			waitHooks{"config-changed", "start"},
  1487  			addMetrics{[]string{"17"}},
  1488  			sendMetricsTick{},
  1489  			checkStateMetrics{number: 2, values: []string{"15", "17"}},
  1490  			verifyRunning{},
  1491  		), ut(
  1492  			"send-metrics state maintained during uniter restart",
  1493  			startupErrorWithCustomCharm{
  1494  				badHook: "config-changed",
  1495  				customize: func(c *gc.C, ctx *context, path string) {
  1496  					ctx.writeMetricsYaml(c, path)
  1497  				},
  1498  			},
  1499  			collectMetricsTick{expectFail: true},
  1500  			addMetrics{[]string{"13"}},
  1501  			sendMetricsTick{expectFail: true},
  1502  			fixHook{"config-changed"},
  1503  			stopUniter{},
  1504  			startUniter{},
  1505  			resolveError{state.ResolvedRetryHooks},
  1506  			waitHooks{"config-changed", "start"},
  1507  			collectMetricsTick{},
  1508  			waitHooks{"collect-metrics"},
  1509  			addMetrics{[]string{"21"}},
  1510  			sendMetricsTick{},
  1511  			checkStateMetrics{number: 2, values: []string{"13", "21"}},
  1512  			verifyRunning{},
  1513  		), ut(
  1514  			"collect-metrics event not triggered for non-metered charm",
  1515  			quickStart{},
  1516  			collectMetricsTick{expectFail: true},
  1517  			addMetrics{[]string{"21"}},
  1518  			sendMetricsTick{expectFail: true},
  1519  			waitHooks{},
  1520  			checkStateMetrics{number: 0},
  1521  		),
  1522  	})
  1523  }
  1524  
  1525  func (s *UniterSuite) TestActionEvents(c *gc.C) {
  1526  	s.runUniterTests(c, []uniterTest{
  1527  		ut(
  1528  			"simple action event: defined in actions.yaml, no args",
  1529  			createCharm{
  1530  				customize: func(c *gc.C, ctx *context, path string) {
  1531  					ctx.writeAction(c, path, "action-log")
  1532  					ctx.writeActionsYaml(c, path, "action-log")
  1533  				},
  1534  			},
  1535  			serveCharm{},
  1536  			ensureStateWorker{},
  1537  			createServiceAndUnit{},
  1538  			startUniter{},
  1539  			waitAddresses{},
  1540  			waitUnitAgent{status: params.StatusIdle},
  1541  			waitUnitAgent{
  1542  				statusGetter: unitStatusGetter,
  1543  				status:       params.StatusUnknown,
  1544  			},
  1545  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1546  			verifyCharm{},
  1547  			addAction{"action-log", nil},
  1548  			waitActionResults{[]actionResult{{
  1549  				name:    "action-log",
  1550  				results: map[string]interface{}{},
  1551  				status:  params.ActionCompleted,
  1552  			}}},
  1553  			waitUnitAgent{status: params.StatusIdle},
  1554  			waitUnitAgent{
  1555  				statusGetter: unitStatusGetter,
  1556  				status:       params.StatusUnknown,
  1557  			},
  1558  		), ut(
  1559  			"action-fail causes the action to fail with a message",
  1560  			createCharm{
  1561  				customize: func(c *gc.C, ctx *context, path string) {
  1562  					ctx.writeAction(c, path, "action-log-fail")
  1563  					ctx.writeActionsYaml(c, path, "action-log-fail")
  1564  				},
  1565  			},
  1566  			serveCharm{},
  1567  			ensureStateWorker{},
  1568  			createServiceAndUnit{},
  1569  			startUniter{},
  1570  			waitAddresses{},
  1571  			waitUnitAgent{status: params.StatusIdle},
  1572  			waitUnitAgent{
  1573  				statusGetter: unitStatusGetter,
  1574  				status:       params.StatusUnknown,
  1575  			},
  1576  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1577  			verifyCharm{},
  1578  			addAction{"action-log-fail", nil},
  1579  			waitActionResults{[]actionResult{{
  1580  				name: "action-log-fail",
  1581  				results: map[string]interface{}{
  1582  					"foo": "still works",
  1583  				},
  1584  				message: "I'm afraid I can't let you do that, Dave.",
  1585  				status:  params.ActionFailed,
  1586  			}}},
  1587  			waitUnitAgent{status: params.StatusIdle}, waitUnitAgent{
  1588  				statusGetter: unitStatusGetter,
  1589  				status:       params.StatusUnknown,
  1590  			},
  1591  		), ut(
  1592  			"action-fail with the wrong arguments fails but is not an error",
  1593  			createCharm{
  1594  				customize: func(c *gc.C, ctx *context, path string) {
  1595  					ctx.writeAction(c, path, "action-log-fail-error")
  1596  					ctx.writeActionsYaml(c, path, "action-log-fail-error")
  1597  				},
  1598  			},
  1599  			serveCharm{},
  1600  			ensureStateWorker{},
  1601  			createServiceAndUnit{},
  1602  			startUniter{},
  1603  			waitAddresses{},
  1604  			waitUnitAgent{status: params.StatusIdle},
  1605  			waitUnitAgent{
  1606  				statusGetter: unitStatusGetter,
  1607  				status:       params.StatusUnknown,
  1608  			},
  1609  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1610  			verifyCharm{},
  1611  			addAction{"action-log-fail-error", nil},
  1612  			waitActionResults{[]actionResult{{
  1613  				name: "action-log-fail-error",
  1614  				results: map[string]interface{}{
  1615  					"foo": "still works",
  1616  				},
  1617  				message: "A real message",
  1618  				status:  params.ActionFailed,
  1619  			}}},
  1620  			waitUnitAgent{status: params.StatusIdle},
  1621  			waitUnitAgent{
  1622  				statusGetter: unitStatusGetter,
  1623  				status:       params.StatusUnknown,
  1624  			},
  1625  		), ut(
  1626  			"actions with correct params passed are not an error",
  1627  			createCharm{
  1628  				customize: func(c *gc.C, ctx *context, path string) {
  1629  					ctx.writeAction(c, path, "snapshot")
  1630  					ctx.writeActionsYaml(c, path, "snapshot")
  1631  				},
  1632  			},
  1633  			serveCharm{},
  1634  			ensureStateWorker{},
  1635  			createServiceAndUnit{},
  1636  			startUniter{},
  1637  			waitAddresses{},
  1638  			waitUnitAgent{status: params.StatusIdle},
  1639  			waitUnitAgent{
  1640  				statusGetter: unitStatusGetter,
  1641  				status:       params.StatusUnknown,
  1642  			},
  1643  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1644  			verifyCharm{},
  1645  			addAction{
  1646  				name:   "snapshot",
  1647  				params: map[string]interface{}{"outfile": "foo.bar"},
  1648  			},
  1649  			waitActionResults{[]actionResult{{
  1650  				name: "snapshot",
  1651  				results: map[string]interface{}{
  1652  					"outfile": map[string]interface{}{
  1653  						"name": "snapshot-01.tar",
  1654  						"size": map[string]interface{}{
  1655  							"magnitude": "10.3",
  1656  							"units":     "GB",
  1657  						},
  1658  					},
  1659  					"completion": "yes",
  1660  				},
  1661  				status: params.ActionCompleted,
  1662  			}}},
  1663  			waitUnitAgent{status: params.StatusIdle},
  1664  			waitUnitAgent{
  1665  				statusGetter: unitStatusGetter,
  1666  				status:       params.StatusUnknown,
  1667  			},
  1668  		), ut(
  1669  			"actions with incorrect params passed are not an error but fail",
  1670  			createCharm{
  1671  				customize: func(c *gc.C, ctx *context, path string) {
  1672  					ctx.writeAction(c, path, "snapshot")
  1673  					ctx.writeActionsYaml(c, path, "snapshot")
  1674  				},
  1675  			},
  1676  			serveCharm{},
  1677  			ensureStateWorker{},
  1678  			createServiceAndUnit{},
  1679  			startUniter{},
  1680  			waitAddresses{},
  1681  			waitUnitAgent{status: params.StatusIdle},
  1682  			waitUnitAgent{
  1683  				statusGetter: unitStatusGetter,
  1684  				status:       params.StatusUnknown,
  1685  			},
  1686  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1687  			verifyCharm{},
  1688  			addAction{
  1689  				name:   "snapshot",
  1690  				params: map[string]interface{}{"outfile": 2},
  1691  			},
  1692  			waitActionResults{[]actionResult{{
  1693  				name:    "snapshot",
  1694  				results: map[string]interface{}{},
  1695  				status:  params.ActionFailed,
  1696  				message: `cannot run "snapshot" action: validation failed: (root).outfile : must be of type string, given 2`,
  1697  			}}},
  1698  			waitUnitAgent{status: params.StatusIdle},
  1699  			waitUnitAgent{
  1700  				statusGetter: unitStatusGetter,
  1701  				status:       params.StatusUnknown,
  1702  			},
  1703  		), ut(
  1704  			"actions not defined in actions.yaml fail without causing a uniter error",
  1705  			createCharm{
  1706  				customize: func(c *gc.C, ctx *context, path string) {
  1707  					ctx.writeAction(c, path, "snapshot")
  1708  				},
  1709  			},
  1710  			serveCharm{},
  1711  			ensureStateWorker{},
  1712  			createServiceAndUnit{},
  1713  			startUniter{},
  1714  			waitAddresses{},
  1715  			waitUnitAgent{status: params.StatusIdle},
  1716  			waitUnitAgent{
  1717  				statusGetter: unitStatusGetter,
  1718  				status:       params.StatusUnknown,
  1719  			},
  1720  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1721  			verifyCharm{},
  1722  			addAction{"snapshot", map[string]interface{}{"outfile": "foo.bar"}},
  1723  			waitActionResults{[]actionResult{{
  1724  				name:    "snapshot",
  1725  				results: map[string]interface{}{},
  1726  				status:  params.ActionFailed,
  1727  				message: `cannot run "snapshot" action: not defined`,
  1728  			}}},
  1729  			waitUnitAgent{status: params.StatusIdle},
  1730  			waitUnitAgent{
  1731  				statusGetter: unitStatusGetter,
  1732  				status:       params.StatusUnknown,
  1733  			},
  1734  		), ut(
  1735  			"pending actions get consumed",
  1736  			createCharm{
  1737  				customize: func(c *gc.C, ctx *context, path string) {
  1738  					ctx.writeAction(c, path, "action-log")
  1739  					ctx.writeActionsYaml(c, path, "action-log")
  1740  				},
  1741  			},
  1742  			serveCharm{},
  1743  			ensureStateWorker{},
  1744  			createServiceAndUnit{},
  1745  			addAction{"action-log", nil},
  1746  			addAction{"action-log", nil},
  1747  			addAction{"action-log", nil},
  1748  			startUniter{},
  1749  			waitAddresses{},
  1750  			waitUnitAgent{status: params.StatusIdle},
  1751  			waitUnitAgent{
  1752  				statusGetter: unitStatusGetter,
  1753  				status:       params.StatusUnknown,
  1754  			},
  1755  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1756  			verifyCharm{},
  1757  			waitActionResults{[]actionResult{{
  1758  				name:    "action-log",
  1759  				results: map[string]interface{}{},
  1760  				status:  params.ActionCompleted,
  1761  			}, {
  1762  				name:    "action-log",
  1763  				results: map[string]interface{}{},
  1764  				status:  params.ActionCompleted,
  1765  			}, {
  1766  				name:    "action-log",
  1767  				results: map[string]interface{}{},
  1768  				status:  params.ActionCompleted,
  1769  			}}},
  1770  			waitUnitAgent{status: params.StatusIdle},
  1771  			waitUnitAgent{
  1772  				statusGetter: unitStatusGetter,
  1773  				status:       params.StatusUnknown,
  1774  			},
  1775  		), ut(
  1776  			"actions not implemented fail but are not errors",
  1777  			createCharm{
  1778  				customize: func(c *gc.C, ctx *context, path string) {
  1779  					ctx.writeActionsYaml(c, path, "action-log")
  1780  				},
  1781  			},
  1782  			serveCharm{},
  1783  			ensureStateWorker{},
  1784  			createServiceAndUnit{},
  1785  			startUniter{},
  1786  			waitAddresses{},
  1787  			waitUnitAgent{status: params.StatusIdle},
  1788  			waitUnitAgent{
  1789  				statusGetter: unitStatusGetter,
  1790  				status:       params.StatusUnknown,
  1791  			},
  1792  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  1793  			verifyCharm{},
  1794  			addAction{"action-log", nil},
  1795  			waitActionResults{[]actionResult{{
  1796  				name:    "action-log",
  1797  				results: map[string]interface{}{},
  1798  				status:  params.ActionFailed,
  1799  				message: `action not implemented on unit "u/0"`,
  1800  			}}},
  1801  			waitUnitAgent{status: params.StatusIdle},
  1802  			waitUnitAgent{
  1803  				statusGetter: unitStatusGetter,
  1804  				status:       params.StatusUnknown,
  1805  			},
  1806  		), ut(
  1807  			"actions may run from ModeHookError, but do not clear the error",
  1808  			startupErrorWithCustomCharm{
  1809  				badHook: "start",
  1810  				customize: func(c *gc.C, ctx *context, path string) {
  1811  					ctx.writeAction(c, path, "action-log")
  1812  					ctx.writeActionsYaml(c, path, "action-log")
  1813  				},
  1814  			},
  1815  			addAction{"action-log", nil},
  1816  			waitUnitAgent{
  1817  				statusGetter: unitStatusGetter,
  1818  				status:       params.StatusError,
  1819  				info:         `hook failed: "start"`,
  1820  				data: map[string]interface{}{
  1821  					"hook": "start",
  1822  				},
  1823  			},
  1824  			waitActionResults{[]actionResult{{
  1825  				name:    "action-log",
  1826  				results: map[string]interface{}{},
  1827  				status:  params.ActionCompleted,
  1828  			}}},
  1829  			waitUnitAgent{
  1830  				statusGetter: unitStatusGetter,
  1831  				status:       params.StatusError,
  1832  				info:         `hook failed: "start"`,
  1833  				data:         map[string]interface{}{"hook": "start"},
  1834  			},
  1835  			verifyWaiting{},
  1836  			resolveError{state.ResolvedNoHooks},
  1837  			waitUnitAgent{status: params.StatusIdle},
  1838  			waitUnitAgent{
  1839  				statusGetter: unitStatusGetter,
  1840  				status:       params.StatusMaintenance,
  1841  				info:         "installing charm software",
  1842  			},
  1843  		),
  1844  	})
  1845  }
  1846  
  1847  func (s *UniterSuite) TestUniterSubordinates(c *gc.C) {
  1848  	s.runUniterTests(c, []uniterTest{
  1849  		// Subordinates.
  1850  		ut(
  1851  			"unit becomes dying while subordinates exist",
  1852  			quickStart{},
  1853  			addSubordinateRelation{"juju-info"},
  1854  			waitSubordinateExists{"logging/0"},
  1855  			unitDying,
  1856  			waitSubordinateDying{},
  1857  			waitHooks{"stop"},
  1858  			verifyWaiting{},
  1859  			removeSubordinate{},
  1860  			waitUniterDead{},
  1861  		), ut(
  1862  			"new subordinate becomes necessary while old one is dying",
  1863  			quickStart{},
  1864  			addSubordinateRelation{"juju-info"},
  1865  			waitSubordinateExists{"logging/0"},
  1866  			removeSubordinateRelation{"juju-info"},
  1867  			// The subordinate Uniter would usually set Dying in this situation.
  1868  			subordinateDying,
  1869  			addSubordinateRelation{"logging-dir"},
  1870  			verifyRunning{},
  1871  			removeSubordinate{},
  1872  			waitSubordinateExists{"logging/1"},
  1873  		),
  1874  	})
  1875  }
  1876  
  1877  func (s *UniterSuite) TestSubordinateDying(c *gc.C) {
  1878  	// Create a test context for later use.
  1879  	ctx := &context{
  1880  		s:                      s,
  1881  		st:                     s.State,
  1882  		path:                   filepath.Join(s.dataDir, "agents", "unit-u-0"),
  1883  		dataDir:                s.dataDir,
  1884  		charms:                 make(map[string][]byte),
  1885  		collectMetricsTicker:   s.collectMetricsTicker,
  1886  		sendMetricsTicker:      s.sendMetricsTicker,
  1887  		updateStatusHookTicker: s.updateStatusHookTicker,
  1888  	}
  1889  
  1890  	addStateServerMachine(c, ctx.st)
  1891  
  1892  	// Create the subordinate service.
  1893  	dir := testcharms.Repo.ClonedDir(c.MkDir(), "logging")
  1894  	curl, err := corecharm.ParseURL("cs:quantal/logging")
  1895  	c.Assert(err, jc.ErrorIsNil)
  1896  	curl = curl.WithRevision(dir.Revision())
  1897  	step(c, ctx, addCharm{dir, curl})
  1898  	ctx.svc = s.AddTestingService(c, "u", ctx.sch)
  1899  
  1900  	// Create the principal service and add a relation.
  1901  	wps := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
  1902  	wpu, err := wps.AddUnit()
  1903  	c.Assert(err, jc.ErrorIsNil)
  1904  	eps, err := s.State.InferEndpoints("wordpress", "u")
  1905  	c.Assert(err, jc.ErrorIsNil)
  1906  	rel, err := s.State.AddRelation(eps...)
  1907  	c.Assert(err, jc.ErrorIsNil)
  1908  	assertAssignUnit(c, s.State, wpu)
  1909  
  1910  	// Create the subordinate unit by entering scope as the principal.
  1911  	wpru, err := rel.Unit(wpu)
  1912  	c.Assert(err, jc.ErrorIsNil)
  1913  	err = wpru.EnterScope(nil)
  1914  	c.Assert(err, jc.ErrorIsNil)
  1915  	ctx.unit, err = s.State.Unit("u/0")
  1916  	c.Assert(err, jc.ErrorIsNil)
  1917  	ctx.apiLogin(c)
  1918  
  1919  	// Run the actual test.
  1920  	ctx.run(c, []stepper{
  1921  		serveCharm{},
  1922  		startUniter{},
  1923  		waitAddresses{},
  1924  		custom{func(c *gc.C, ctx *context) {
  1925  			c.Assert(rel.Destroy(), gc.IsNil)
  1926  		}},
  1927  		waitUniterDead{},
  1928  	})
  1929  }
  1930  
  1931  func (s *UniterSuite) TestReboot(c *gc.C) {
  1932  	s.runUniterTests(c, []uniterTest{
  1933  		ut(
  1934  			"test that juju-reboot disabled in actions",
  1935  			createCharm{
  1936  				customize: func(c *gc.C, ctx *context, path string) {
  1937  					ctx.writeAction(c, path, "action-reboot")
  1938  					ctx.writeActionsYaml(c, path, "action-reboot")
  1939  				},
  1940  			},
  1941  			serveCharm{},
  1942  			ensureStateWorker{},
  1943  			createServiceAndUnit{},
  1944  			addAction{"action-reboot", nil},
  1945  			startUniter{},
  1946  			waitAddresses{},
  1947  			waitUnitAgent{
  1948  				status: params.StatusIdle,
  1949  			},
  1950  			waitUnitAgent{
  1951  				statusGetter: unitStatusGetter,
  1952  				status:       params.StatusUnknown,
  1953  			},
  1954  			waitActionResults{[]actionResult{{
  1955  				name: "action-reboot",
  1956  				results: map[string]interface{}{
  1957  					"reboot-delayed": "good",
  1958  					"reboot-now":     "good",
  1959  				},
  1960  				status: params.ActionCompleted,
  1961  			}}},
  1962  		), ut(
  1963  			"test that juju-reboot finishes hook, and reboots",
  1964  			createCharm{
  1965  				customize: func(c *gc.C, ctx *context, path string) {
  1966  					hpath := filepath.Join(path, "hooks", "install")
  1967  					ctx.writeExplicitHook(c, hpath, rebootHook)
  1968  				},
  1969  			},
  1970  			serveCharm{},
  1971  			ensureStateWorker{},
  1972  			createServiceAndUnit{},
  1973  			startUniter{},
  1974  			waitAddresses{},
  1975  			waitUniterDead{err: "machine needs to reboot"},
  1976  			waitHooks{"install"},
  1977  			startUniter{},
  1978  			waitUnitAgent{
  1979  				status: params.StatusIdle,
  1980  			},
  1981  			waitUnitAgent{
  1982  				statusGetter: unitStatusGetter,
  1983  				status:       params.StatusUnknown,
  1984  			},
  1985  			waitHooks{"leader-elected", "config-changed", "start"},
  1986  		), ut(
  1987  			"test that juju-reboot --now kills hook and exits",
  1988  			createCharm{
  1989  				customize: func(c *gc.C, ctx *context, path string) {
  1990  					hpath := filepath.Join(path, "hooks", "install")
  1991  					ctx.writeExplicitHook(c, hpath, rebootNowHook)
  1992  				},
  1993  			},
  1994  			serveCharm{},
  1995  			ensureStateWorker{},
  1996  			createServiceAndUnit{},
  1997  			startUniter{},
  1998  			waitAddresses{},
  1999  			waitUniterDead{err: "machine needs to reboot"},
  2000  			waitHooks{"install"},
  2001  			startUniter{},
  2002  			waitUnitAgent{
  2003  				status: params.StatusIdle,
  2004  			},
  2005  			waitUnitAgent{
  2006  				statusGetter: unitStatusGetter,
  2007  				status:       params.StatusUnknown,
  2008  			},
  2009  			waitHooks{"install", "leader-elected", "config-changed", "start"},
  2010  		), ut(
  2011  			"test juju-reboot will not happen if hook errors out",
  2012  			createCharm{
  2013  				customize: func(c *gc.C, ctx *context, path string) {
  2014  					hpath := filepath.Join(path, "hooks", "install")
  2015  					ctx.writeExplicitHook(c, hpath, badRebootHook)
  2016  				},
  2017  			},
  2018  			serveCharm{},
  2019  			ensureStateWorker{},
  2020  			createServiceAndUnit{},
  2021  			startUniter{},
  2022  			waitAddresses{},
  2023  			waitUnitAgent{
  2024  				statusGetter: unitStatusGetter,
  2025  				status:       params.StatusError,
  2026  				info:         fmt.Sprintf(`hook failed: "install"`),
  2027  			},
  2028  		),
  2029  	})
  2030  }
  2031  
  2032  func (s *UniterSuite) TestRebootFromJujuRun(c *gc.C) {
  2033  	//TODO(bogdanteleaga): Fix this on windows
  2034  	if runtime.GOOS == "windows" {
  2035  		c.Skip("bug 1403084: currently does not work on windows")
  2036  	}
  2037  	s.runUniterTests(c, []uniterTest{
  2038  		ut(
  2039  			"test juju-reboot",
  2040  			quickStart{},
  2041  			runCommands{"juju-reboot"},
  2042  			waitUniterDead{err: "machine needs to reboot"},
  2043  			startUniter{},
  2044  			waitHooks{"config-changed"},
  2045  		), ut(
  2046  			"test juju-reboot with bad hook",
  2047  			startupError{"install"},
  2048  			runCommands{"juju-reboot"},
  2049  			waitUniterDead{err: "machine needs to reboot"},
  2050  			startUniter{},
  2051  			waitHooks{},
  2052  		), ut(
  2053  			"test juju-reboot --now",
  2054  			quickStart{},
  2055  			runCommands{"juju-reboot --now"},
  2056  			waitUniterDead{err: "machine needs to reboot"},
  2057  			startUniter{},
  2058  			waitHooks{"config-changed"},
  2059  		), ut(
  2060  			"test juju-reboot --now with bad hook",
  2061  			startupError{"install"},
  2062  			runCommands{"juju-reboot --now"},
  2063  			waitUniterDead{err: "machine needs to reboot"},
  2064  			startUniter{},
  2065  			waitHooks{},
  2066  		),
  2067  	})
  2068  }
  2069  
  2070  func (s *UniterSuite) TestLeadership(c *gc.C) {
  2071  	s.runUniterTests(c, []uniterTest{
  2072  		ut(
  2073  			"hook tools when leader",
  2074  			quickStart{},
  2075  			runCommands{"leader-set foo=bar baz=qux"},
  2076  			verifyLeaderSettings{"foo": "bar", "baz": "qux"},
  2077  		), ut(
  2078  			"hook tools when not leader",
  2079  			quickStart{minion: true},
  2080  			runCommands{leadershipScript},
  2081  		), ut(
  2082  			"leader-elected triggers when elected",
  2083  			quickStart{minion: true},
  2084  			forceLeader{},
  2085  			waitHooks{"leader-elected"},
  2086  		), ut(
  2087  			"leader-settings-changed triggers when leader settings change",
  2088  			quickStart{minion: true},
  2089  			setLeaderSettings{"ping": "pong"},
  2090  			waitHooks{"leader-settings-changed"},
  2091  		), ut(
  2092  			"leader-settings-changed triggers when bounced",
  2093  			quickStart{minion: true},
  2094  			verifyRunning{minion: true},
  2095  		), ut(
  2096  			"leader-settings-changed triggers when deposed (while stopped)",
  2097  			quickStart{},
  2098  			stopUniter{},
  2099  			forceMinion{},
  2100  			verifyRunning{minion: true},
  2101  		),
  2102  	})
  2103  }
  2104  
  2105  func (s *UniterSuite) TestLeadershipUnexpectedDepose(c *gc.C) {
  2106  	s.PatchValue(uniter.LeadershipGuarantee, 2*coretesting.ShortWait)
  2107  	s.runUniterTests(c, []uniterTest{
  2108  		ut(
  2109  			// NOTE: this is a strange and ugly test, intended to detect what
  2110  			// *would* happen if the uniter suddenly failed to renew its lease;
  2111  			// it depends on an artificially shortened tracker refresh time to
  2112  			// run in a reasonable amount of time.
  2113  			"leader-settings-changed triggers when deposed (while running)",
  2114  			quickStart{},
  2115  			forceMinion{},
  2116  			waitHooks{"leader-settings-changed"},
  2117  		),
  2118  	})
  2119  }
  2120  
  2121  func (s *UniterSuite) TestStorage(c *gc.C) {
  2122  	// appendStorageMetadata customises the wordpress charm's metadata,
  2123  	// adding a "wp-content" filesystem store. We do it here rather
  2124  	// than in the charm itself to avoid modifying all of the other
  2125  	// scenarios.
  2126  	appendStorageMetadata := func(c *gc.C, ctx *context, path string) {
  2127  		f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644)
  2128  		c.Assert(err, jc.ErrorIsNil)
  2129  		defer func() {
  2130  			err := f.Close()
  2131  			c.Assert(err, jc.ErrorIsNil)
  2132  		}()
  2133  		_, err = io.WriteString(f, "storage:\n  wp-content:\n    type: filesystem\n")
  2134  		c.Assert(err, jc.ErrorIsNil)
  2135  	}
  2136  	s.runUniterTests(c, []uniterTest{
  2137  		ut(
  2138  			"test that storage-attached is called",
  2139  			createCharm{customize: appendStorageMetadata},
  2140  			serveCharm{},
  2141  			ensureStateWorker{},
  2142  			createServiceAndUnit{},
  2143  			provisionStorage{},
  2144  			startUniter{},
  2145  			waitAddresses{},
  2146  			waitHooks{"wp-content-storage-attached"},
  2147  			waitHooks(startupHooks(false)),
  2148  		), ut(
  2149  			"test that storage-detaching is called before stop",
  2150  			createCharm{customize: appendStorageMetadata},
  2151  			serveCharm{},
  2152  			ensureStateWorker{},
  2153  			createServiceAndUnit{},
  2154  			provisionStorage{},
  2155  			startUniter{},
  2156  			waitAddresses{},
  2157  			waitHooks{"wp-content-storage-attached"},
  2158  			waitHooks(startupHooks(false)),
  2159  			unitDying,
  2160  			waitHooks{"leader-settings-changed"},
  2161  			// "stop" hook is not called until storage is detached
  2162  			waitHooks{"wp-content-storage-detaching", "stop"},
  2163  			verifyStorageDetached{},
  2164  			waitUniterDead{},
  2165  		), ut(
  2166  			"test that storage-detaching is called only if previously attached",
  2167  			createCharm{customize: appendStorageMetadata},
  2168  			serveCharm{},
  2169  			ensureStateWorker{},
  2170  			createServiceAndUnit{},
  2171  			// provision and destroy the storage before the uniter starts,
  2172  			// to ensure it never sees the storage as attached
  2173  			provisionStorage{},
  2174  			destroyStorageAttachment{},
  2175  			startUniter{},
  2176  			waitHooks(startupHooks(false)),
  2177  			unitDying,
  2178  			// storage-detaching is not called because it was never attached
  2179  			waitHooks{"stop"},
  2180  			verifyStorageDetached{},
  2181  			waitUniterDead{},
  2182  		), ut(
  2183  			"test that delay-provisioned storage does not block forever",
  2184  			createCharm{customize: appendStorageMetadata},
  2185  			serveCharm{},
  2186  			ensureStateWorker{},
  2187  			createServiceAndUnit{},
  2188  			startUniter{},
  2189  			// no hooks should be run, as storage isn't provisioned
  2190  			waitHooks{},
  2191  			provisionStorage{},
  2192  			waitHooks{"wp-content-storage-attached"},
  2193  			waitHooks(startupHooks(false)),
  2194  		), ut(
  2195  			"test that unprovisioned storage does not block unit termination",
  2196  			createCharm{customize: appendStorageMetadata},
  2197  			serveCharm{},
  2198  			ensureStateWorker{},
  2199  			createServiceAndUnit{},
  2200  			startUniter{},
  2201  			// no hooks should be run, as storage isn't provisioned
  2202  			waitHooks{},
  2203  			unitDying,
  2204  			// TODO(axw) should we really be running startup hooks
  2205  			// when the unit is dying?
  2206  			waitHooks(startupHooks(true)),
  2207  			waitHooks{"stop"},
  2208  			waitUniterDead{},
  2209  		),
  2210  		// TODO(axw) test that storage-attached is run for new
  2211  		// storage attachments before upgrade-charm is run. This
  2212  		// requires additions to state to add storage when a charm
  2213  		// is upgraded.
  2214  	})
  2215  }
  2216  
  2217  type mockExecutor struct {
  2218  	operation.Executor
  2219  }
  2220  
  2221  func (m *mockExecutor) Run(op operation.Operation) error {
  2222  	// want to allow charm unpacking to occur
  2223  	if strings.HasPrefix(op.String(), "install") {
  2224  		return m.Executor.Run(op)
  2225  	}
  2226  	// but hooks should error
  2227  	return errors.New("some error occurred")
  2228  }
  2229  
  2230  func (s *UniterSuite) TestOperationErrorReported(c *gc.C) {
  2231  	executorFunc := func(stateFilePath string, getInstallCharm func() (*corecharm.URL, error), acquireLock func(string) (func() error, error)) (operation.Executor, error) {
  2232  		e, err := operation.NewExecutor(stateFilePath, getInstallCharm, acquireLock)
  2233  		c.Assert(err, jc.ErrorIsNil)
  2234  		return &mockExecutor{e}, nil
  2235  	}
  2236  	s.runUniterTests(c, []uniterTest{
  2237  		ut(
  2238  			"error running operations are reported",
  2239  			createCharm{},
  2240  			serveCharm{},
  2241  			createUniter{executorFunc: executorFunc},
  2242  			waitUnitAgent{
  2243  				status: params.StatusFailed,
  2244  				info:   "run install hook",
  2245  			},
  2246  			expectError{".*some error occurred.*"},
  2247  		),
  2248  	})
  2249  }