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