github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/allwatcher_internal_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/juju/charm/v12"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/names/v5"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/utils/v3"
    17  	"github.com/juju/version/v2"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/core/constraints"
    21  	"github.com/juju/juju/core/crossmodel"
    22  	"github.com/juju/juju/core/instance"
    23  	"github.com/juju/juju/core/life"
    24  	"github.com/juju/juju/core/model"
    25  	coremodel "github.com/juju/juju/core/model"
    26  	"github.com/juju/juju/core/multiwatcher"
    27  	"github.com/juju/juju/core/network"
    28  	corenetwork "github.com/juju/juju/core/network"
    29  	"github.com/juju/juju/core/permission"
    30  	"github.com/juju/juju/core/status"
    31  	"github.com/juju/juju/state/watcher"
    32  	"github.com/juju/juju/testing"
    33  )
    34  
    35  const allEndpoints = ""
    36  
    37  var (
    38  	_ backingEntityDoc = (*backingMachine)(nil)
    39  	_ backingEntityDoc = (*backingUnit)(nil)
    40  	_ backingEntityDoc = (*backingApplication)(nil)
    41  	_ backingEntityDoc = (*backingCharm)(nil)
    42  	_ backingEntityDoc = (*backingRemoteApplication)(nil)
    43  	_ backingEntityDoc = (*backingApplicationOffer)(nil)
    44  	_ backingEntityDoc = (*backingRelation)(nil)
    45  	_ backingEntityDoc = (*backingAnnotation)(nil)
    46  	_ backingEntityDoc = (*backingStatus)(nil)
    47  	_ backingEntityDoc = (*backingConstraints)(nil)
    48  	_ backingEntityDoc = (*backingSettings)(nil)
    49  	_ backingEntityDoc = (*backingOpenedPorts)(nil)
    50  	_ backingEntityDoc = (*backingAction)(nil)
    51  	_ backingEntityDoc = (*backingBlock)(nil)
    52  	_ backingEntityDoc = (*backingGeneration)(nil)
    53  	_ backingEntityDoc = (*backingPodSpec)(nil)
    54  )
    55  
    56  var dottedConfig = `
    57  options:
    58    key.dotted: {default: My Key, description: Desc, type: string}
    59  `
    60  
    61  type allWatcherBaseSuite struct {
    62  	internalStateSuite
    63  	currentTime time.Time
    64  }
    65  
    66  func (s *allWatcherBaseSuite) SetUpTest(c *gc.C) {
    67  	s.internalStateSuite.SetUpTest(c)
    68  	s.currentTime = s.state.clock().Now()
    69  	loggo.GetLogger("juju.state.allwatcher").SetLogLevel(loggo.TRACE)
    70  }
    71  
    72  // setUpScenario adds some entities to the state so that
    73  // we can check that they all get pulled in by
    74  // all(Model)WatcherStateBacking.GetAll.
    75  func (s *allWatcherBaseSuite) setUpScenario(c *gc.C, st *State, units int) (entities entityInfoSlice) {
    76  	modelUUID := st.ModelUUID()
    77  	model, err := st.Model()
    78  	c.Assert(err, jc.ErrorIsNil)
    79  	add := func(e multiwatcher.EntityInfo) {
    80  		entities = append(entities, e)
    81  	}
    82  
    83  	modelCfg, err := model.Config()
    84  	c.Assert(err, jc.ErrorIsNil)
    85  	modelStatus, err := model.Status()
    86  	c.Assert(err, jc.ErrorIsNil)
    87  	credential, _ := model.CloudCredentialTag()
    88  	add(&multiwatcher.ModelInfo{
    89  		ModelUUID:       model.UUID(),
    90  		Type:            coremodel.ModelType(model.Type()),
    91  		Name:            model.Name(),
    92  		Life:            life.Alive,
    93  		Owner:           model.Owner().Id(),
    94  		ControllerUUID:  model.ControllerUUID(),
    95  		IsController:    model.IsControllerModel(),
    96  		Cloud:           model.CloudName(),
    97  		CloudRegion:     model.CloudRegion(),
    98  		CloudCredential: credential.Id(),
    99  		Config:          modelCfg.AllAttrs(),
   100  		Status: multiwatcher.StatusInfo{
   101  			Current: modelStatus.Status,
   102  			Message: modelStatus.Message,
   103  			Data:    modelStatus.Data,
   104  			Since:   modelStatus.Since,
   105  		},
   106  		SLA: multiwatcher.ModelSLAInfo{
   107  			Level: "unsupported",
   108  		},
   109  		UserPermissions: map[string]permission.Access{
   110  			"test-admin": permission.AdminAccess,
   111  		},
   112  	})
   113  
   114  	now := s.currentTime
   115  	m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
   116  	c.Assert(err, jc.ErrorIsNil)
   117  	c.Assert(m.Tag(), gc.Equals, names.NewMachineTag("0"))
   118  	// Ensure there's one and only one controller.
   119  	controllerIds, err := st.ControllerIds()
   120  	c.Assert(err, jc.ErrorIsNil)
   121  	needController := len(controllerIds) == 0
   122  	if needController {
   123  		_, err = st.EnableHA(1, constraints.Value{}, UbuntuBase("20.04"), []string{m.Id()})
   124  		c.Assert(err, jc.ErrorIsNil)
   125  		node, err := st.ControllerNode(m.Id())
   126  		c.Assert(err, jc.ErrorIsNil)
   127  		err = node.SetHasVote(true)
   128  		c.Assert(err, jc.ErrorIsNil)
   129  	}
   130  	// TODO(dfc) instance.Id should take a TAG!
   131  	err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", nil)
   132  	c.Assert(err, jc.ErrorIsNil)
   133  	hc, err := m.HardwareCharacteristics()
   134  	c.Assert(err, jc.ErrorIsNil)
   135  	cp, err := m.CharmProfiles()
   136  	c.Assert(err, jc.ErrorIsNil)
   137  
   138  	// Add a space and an address on the space.
   139  	space, err := st.AddSpace("test-space", "provider-space", nil, true)
   140  	c.Assert(err, jc.ErrorIsNil)
   141  	providerAddr := network.NewSpaceAddress("example.com")
   142  	providerAddr.SpaceID = space.Id()
   143  	err = m.SetProviderAddresses(providerAddr)
   144  	c.Assert(err, jc.ErrorIsNil)
   145  
   146  	var addresses []network.ProviderAddress
   147  	for _, addr := range m.Addresses() {
   148  		addresses = append(addresses, network.ProviderAddress{
   149  			MachineAddress:  addr.MachineAddress,
   150  			SpaceName:       network.SpaceName(space.Name()),
   151  			ProviderSpaceID: space.ProviderId(),
   152  		})
   153  	}
   154  
   155  	jobs := []coremodel.MachineJob{JobHostUnits.ToParams()}
   156  	if needController {
   157  		jobs = append(jobs, JobManageModel.ToParams())
   158  	}
   159  	add(&multiwatcher.MachineInfo{
   160  		ModelUUID:  modelUUID,
   161  		ID:         "0",
   162  		InstanceID: "i-machine-0",
   163  		AgentStatus: multiwatcher.StatusInfo{
   164  			Current: status.Pending,
   165  			Data:    map[string]interface{}{},
   166  			Since:   &now,
   167  		},
   168  		InstanceStatus: multiwatcher.StatusInfo{
   169  			Current: status.Pending,
   170  			Data:    map[string]interface{}{},
   171  			Since:   &now,
   172  		},
   173  		Life:                    life.Alive,
   174  		Base:                    "ubuntu@12.10",
   175  		Jobs:                    jobs,
   176  		Addresses:               addresses,
   177  		HardwareCharacteristics: hc,
   178  		CharmProfiles:           cp,
   179  		HasVote:                 needController,
   180  		WantsVote:               needController,
   181  		PreferredPublicAddress:  providerAddr,
   182  		PreferredPrivateAddress: providerAddr,
   183  	})
   184  
   185  	wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   186  	err = wordpress.MergeExposeSettings(nil)
   187  	c.Assert(err, jc.ErrorIsNil)
   188  	err = wordpress.SetMinUnits(units)
   189  	c.Assert(err, jc.ErrorIsNil)
   190  	err = wordpress.SetConstraints(constraints.MustParse("mem=100M"))
   191  	c.Assert(err, jc.ErrorIsNil)
   192  	setApplicationConfigAttr(c, wordpress, "blog-title", "boring")
   193  	pairs := map[string]string{"x": "12", "y": "99"}
   194  	add(&multiwatcher.ApplicationInfo{
   195  		ModelUUID:   modelUUID,
   196  		Name:        "wordpress",
   197  		Exposed:     true,
   198  		CharmURL:    applicationCharmURL(wordpress).String(),
   199  		Life:        life.Alive,
   200  		MinUnits:    units,
   201  		Constraints: constraints.MustParse("mem=100M"),
   202  		Annotations: pairs,
   203  		Config:      charm.Settings{"blog-title": "boring"},
   204  		Subordinate: false,
   205  		Status: multiwatcher.StatusInfo{
   206  			Current: "unset",
   207  			Message: "",
   208  			Data:    map[string]interface{}{},
   209  			Since:   &now,
   210  		},
   211  	})
   212  	err = model.SetAnnotations(wordpress, pairs)
   213  	c.Assert(err, jc.ErrorIsNil)
   214  	add(&multiwatcher.AnnotationInfo{
   215  		ModelUUID:   modelUUID,
   216  		Tag:         "application-wordpress",
   217  		Annotations: pairs,
   218  	})
   219  
   220  	add(&multiwatcher.CharmInfo{
   221  		ModelUUID:     modelUUID,
   222  		CharmURL:      applicationCharmURL(wordpress).String(),
   223  		Life:          life.Alive,
   224  		DefaultConfig: map[string]interface{}{"blog-title": "My Title"},
   225  	})
   226  
   227  	logging := AddTestingApplication(c, st, "logging", AddTestingCharm(c, st, "logging"))
   228  	add(&multiwatcher.ApplicationInfo{
   229  		ModelUUID:   modelUUID,
   230  		Name:        "logging",
   231  		CharmURL:    applicationCharmURL(logging).String(),
   232  		Life:        life.Alive,
   233  		Config:      charm.Settings{},
   234  		Subordinate: true,
   235  		Status: multiwatcher.StatusInfo{
   236  			Current: "unset",
   237  			Message: "",
   238  			Data:    map[string]interface{}{},
   239  			Since:   &now,
   240  		},
   241  	})
   242  
   243  	add(&multiwatcher.CharmInfo{
   244  		ModelUUID: modelUUID,
   245  		CharmURL:  applicationCharmURL(logging).String(),
   246  		Life:      life.Alive,
   247  	})
   248  
   249  	eps, err := st.InferEndpoints("logging", "wordpress")
   250  	c.Assert(err, jc.ErrorIsNil)
   251  	rel, err := st.AddRelation(eps...)
   252  	c.Assert(err, jc.ErrorIsNil)
   253  	add(&multiwatcher.RelationInfo{
   254  		ModelUUID: modelUUID,
   255  		Key:       "logging:logging-directory wordpress:logging-dir",
   256  		ID:        rel.Id(),
   257  		Endpoints: []multiwatcher.Endpoint{
   258  			{ApplicationName: "logging", Relation: multiwatcher.CharmRelation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}},
   259  			{ApplicationName: "wordpress", Relation: multiwatcher.CharmRelation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}},
   260  	})
   261  
   262  	for i := 0; i < units; i++ {
   263  		wu, err := wordpress.AddUnit(AddUnitParams{})
   264  		c.Assert(err, jc.ErrorIsNil)
   265  		c.Assert(wu.Tag().String(), gc.Equals, fmt.Sprintf("unit-wordpress-%d", i))
   266  
   267  		m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
   268  		c.Assert(err, jc.ErrorIsNil)
   269  		c.Assert(m.Tag().String(), gc.Equals, fmt.Sprintf("machine-%d", i+1))
   270  
   271  		pairs := map[string]string{"name": fmt.Sprintf("bar %d", i)}
   272  		add(&multiwatcher.UnitInfo{
   273  			ModelUUID:   modelUUID,
   274  			Name:        fmt.Sprintf("wordpress/%d", i),
   275  			Application: wordpress.Name(),
   276  			Base:        "ubuntu@12.10",
   277  			Life:        life.Alive,
   278  			MachineID:   m.Id(),
   279  			Annotations: pairs,
   280  			Subordinate: false,
   281  			WorkloadStatus: multiwatcher.StatusInfo{
   282  				Current: "waiting",
   283  				Message: "waiting for machine",
   284  				Data:    map[string]interface{}{},
   285  				Since:   &now,
   286  			},
   287  			AgentStatus: multiwatcher.StatusInfo{
   288  				Current: "allocating",
   289  				Message: "",
   290  				Data:    map[string]interface{}{},
   291  				Since:   &now,
   292  			},
   293  		})
   294  		err = model.SetAnnotations(wu, pairs)
   295  		c.Assert(err, jc.ErrorIsNil)
   296  		add(&multiwatcher.AnnotationInfo{
   297  			ModelUUID:   modelUUID,
   298  			Tag:         fmt.Sprintf("unit-wordpress-%d", i),
   299  			Annotations: pairs,
   300  		})
   301  		err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", nil)
   302  		c.Assert(err, jc.ErrorIsNil)
   303  		sInfo := status.StatusInfo{
   304  			Status:  status.Error,
   305  			Message: m.Tag().String(),
   306  			Since:   &now,
   307  		}
   308  		err = m.SetStatus(sInfo)
   309  		c.Assert(err, jc.ErrorIsNil)
   310  		hc, err := m.HardwareCharacteristics()
   311  		c.Assert(err, jc.ErrorIsNil)
   312  		cp, err := m.CharmProfiles()
   313  		c.Assert(err, jc.ErrorIsNil)
   314  		add(&multiwatcher.MachineInfo{
   315  			ModelUUID:  modelUUID,
   316  			ID:         fmt.Sprint(i + 1),
   317  			InstanceID: "i-" + m.Tag().String(),
   318  			AgentStatus: multiwatcher.StatusInfo{
   319  				Current: status.Error,
   320  				Message: m.Tag().String(),
   321  				Data:    map[string]interface{}{},
   322  				Since:   &now,
   323  			},
   324  			InstanceStatus: multiwatcher.StatusInfo{
   325  				Current: status.Pending,
   326  				Data:    map[string]interface{}{},
   327  				Since:   &now,
   328  			},
   329  			Life:                    life.Alive,
   330  			Base:                    "ubuntu@12.10",
   331  			Jobs:                    []coremodel.MachineJob{JobHostUnits.ToParams()},
   332  			Addresses:               []network.ProviderAddress{},
   333  			HardwareCharacteristics: hc,
   334  			CharmProfiles:           cp,
   335  			HasVote:                 false,
   336  			WantsVote:               false,
   337  		})
   338  		err = wu.AssignToMachine(m)
   339  		c.Assert(err, jc.ErrorIsNil)
   340  
   341  		wru, err := rel.Unit(wu)
   342  		c.Assert(err, jc.ErrorIsNil)
   343  
   344  		// Create the subordinate unit as a side-effect of entering
   345  		// scope in the principal's relation-unit.
   346  		err = wru.EnterScope(nil)
   347  		c.Assert(err, jc.ErrorIsNil)
   348  
   349  		lu, err := st.Unit(fmt.Sprintf("logging/%d", i))
   350  		c.Assert(err, jc.ErrorIsNil)
   351  		c.Assert(lu.IsPrincipal(), jc.IsFalse)
   352  		unitName := fmt.Sprintf("wordpress/%d", i)
   353  		add(&multiwatcher.UnitInfo{
   354  			ModelUUID:   modelUUID,
   355  			Name:        fmt.Sprintf("logging/%d", i),
   356  			Application: "logging",
   357  			Base:        "ubuntu@12.10",
   358  			Life:        life.Alive,
   359  			MachineID:   m.Id(),
   360  			Principal:   unitName,
   361  			Subordinate: true,
   362  			WorkloadStatus: multiwatcher.StatusInfo{
   363  				Current: "waiting",
   364  				Message: "waiting for machine",
   365  				Data:    map[string]interface{}{},
   366  				Since:   &now,
   367  			},
   368  			AgentStatus: multiwatcher.StatusInfo{
   369  				Current: "allocating",
   370  				Message: "",
   371  				Data:    map[string]interface{}{},
   372  				Since:   &now,
   373  			},
   374  		})
   375  	}
   376  
   377  	_, remoteApplicationInfo := addTestingRemoteApplication(
   378  		c, st, "remote-mysql1", "me/model.mysql", mysqlRelations, false,
   379  	)
   380  	add(&remoteApplicationInfo)
   381  
   382  	mysql := AddTestingApplication(c, st, "mysql", AddTestingCharm(c, st, "mysql"))
   383  	curl := applicationCharmURL(mysql)
   384  	add(&multiwatcher.ApplicationInfo{
   385  		ModelUUID:   modelUUID,
   386  		Name:        "mysql",
   387  		CharmURL:    curl.String(),
   388  		Life:        life.Alive,
   389  		Config:      charm.Settings{},
   390  		Constraints: constraints.MustParse("arch=amd64"),
   391  		Status: multiwatcher.StatusInfo{
   392  			Current: "unset",
   393  			Message: "",
   394  			Data:    map[string]interface{}{},
   395  			Since:   &now,
   396  		},
   397  	})
   398  
   399  	add(&multiwatcher.CharmInfo{
   400  		ModelUUID:     modelUUID,
   401  		CharmURL:      applicationCharmURL(mysql).String(),
   402  		Life:          life.Alive,
   403  		DefaultConfig: map[string]interface{}{"dataset-size": "80%"},
   404  	})
   405  
   406  	// Set up a remote application related to the offer.
   407  	// It won't be included in the backing model.
   408  	addTestingRemoteApplication(
   409  		c, st, "remote-wordpress", "", []charm.Relation{{
   410  			Name:      "db",
   411  			Role:      "requirer",
   412  			Scope:     charm.ScopeGlobal,
   413  			Interface: "mysql",
   414  		}}, true,
   415  	)
   416  	addTestingRemoteApplication(
   417  		c, st, "remote-wordpress2", "", []charm.Relation{{
   418  			Name:      "db",
   419  			Role:      "requirer",
   420  			Scope:     charm.ScopeGlobal,
   421  			Interface: "mysql",
   422  		}}, true,
   423  	)
   424  	eps, err = st.InferEndpoints("mysql", "remote-wordpress2")
   425  	c.Assert(err, jc.ErrorIsNil)
   426  	rel, err = st.AddRelation(eps...)
   427  	c.Assert(err, jc.ErrorIsNil)
   428  	add(&multiwatcher.RelationInfo{
   429  		ModelUUID: modelUUID,
   430  		Key:       rel.Tag().Id(),
   431  		ID:        rel.Id(),
   432  		Endpoints: []multiwatcher.Endpoint{
   433  			{ApplicationName: "mysql", Relation: multiwatcher.CharmRelation{Name: "server", Role: "provider", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}},
   434  			{ApplicationName: "remote-wordpress2", Relation: multiwatcher.CharmRelation{Name: "db", Role: "requirer", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}},
   435  	})
   436  
   437  	applicationOfferInfo, rel2 := addTestingApplicationOffer(
   438  		c, st, s.owner, "hosted-mysql", "mysql", curl.Name, []string{"server"},
   439  	)
   440  	add(&multiwatcher.RelationInfo{
   441  		ModelUUID: modelUUID,
   442  		Key:       rel2.Tag().Id(),
   443  		ID:        rel2.Id(),
   444  		Endpoints: []multiwatcher.Endpoint{
   445  			{ApplicationName: "mysql", Relation: multiwatcher.CharmRelation{Name: "server", Role: "provider", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}},
   446  			{ApplicationName: "remote-wordpress", Relation: multiwatcher.CharmRelation{Name: "db", Role: "requirer", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}},
   447  	})
   448  	add(&applicationOfferInfo)
   449  
   450  	return
   451  }
   452  
   453  var mysqlRelations = []charm.Relation{{
   454  	Name:      "db",
   455  	Role:      "provider",
   456  	Scope:     charm.ScopeGlobal,
   457  	Interface: "mysql",
   458  }, {
   459  	Name:      "nrpe-external-master",
   460  	Role:      "provider",
   461  	Scope:     charm.ScopeGlobal,
   462  	Interface: "nrpe-external-master",
   463  }}
   464  
   465  func addTestingRemoteApplication(
   466  	c *gc.C, st *State, name, url string, relations []charm.Relation, isProxy bool,
   467  ) (*RemoteApplication, multiwatcher.RemoteApplicationUpdate) {
   468  
   469  	rs, err := st.AddRemoteApplication(AddRemoteApplicationParams{
   470  		Name:            name,
   471  		URL:             url,
   472  		SourceModel:     testing.ModelTag,
   473  		Endpoints:       relations,
   474  		IsConsumerProxy: isProxy,
   475  	})
   476  	c.Assert(err, jc.ErrorIsNil)
   477  	var appStatus multiwatcher.StatusInfo
   478  	if !isProxy {
   479  		status, err := rs.Status()
   480  		c.Assert(err, jc.ErrorIsNil)
   481  		appStatus = multiwatcher.StatusInfo{
   482  			Current: status.Status,
   483  			Message: status.Message,
   484  			Data:    status.Data,
   485  			Since:   status.Since,
   486  		}
   487  	}
   488  	return rs, multiwatcher.RemoteApplicationUpdate{
   489  		ModelUUID: st.ModelUUID(),
   490  		Name:      name,
   491  		OfferURL:  url,
   492  		Life:      life.Value(rs.Life().String()),
   493  		Status:    appStatus,
   494  	}
   495  }
   496  
   497  func addTestingApplicationOffer(
   498  	c *gc.C, st *State, owner names.UserTag, offerName, applicationName, charmName string, endpoints []string,
   499  ) (multiwatcher.ApplicationOfferInfo, *Relation) {
   500  
   501  	eps := make(map[string]string)
   502  	for _, ep := range endpoints {
   503  		eps[ep] = ep
   504  	}
   505  	offers := NewApplicationOffers(st)
   506  	offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{
   507  		OfferName:       offerName,
   508  		Owner:           owner.Name(),
   509  		ApplicationName: applicationName,
   510  		Endpoints:       eps,
   511  	})
   512  	c.Assert(err, jc.ErrorIsNil)
   513  	relEps, err := st.InferEndpoints("mysql", "remote-wordpress")
   514  	c.Assert(err, jc.ErrorIsNil)
   515  	rel, err := st.AddRelation(relEps...)
   516  	c.Assert(err, jc.ErrorIsNil)
   517  	err = rel.SetStatus(status.StatusInfo{Status: status.Joined})
   518  	c.Assert(err, jc.ErrorIsNil)
   519  	_, err = st.AddOfferConnection(AddOfferConnectionParams{
   520  		SourceModelUUID: utils.MustNewUUID().String(),
   521  		RelationId:      rel.Id(),
   522  		Username:        "fred",
   523  		OfferUUID:       offer.OfferUUID,
   524  		RelationKey:     rel.Tag().Id(),
   525  	})
   526  	c.Assert(err, jc.ErrorIsNil)
   527  	return multiwatcher.ApplicationOfferInfo{
   528  		ModelUUID:            st.ModelUUID(),
   529  		OfferName:            offerName,
   530  		OfferUUID:            offer.OfferUUID,
   531  		ApplicationName:      applicationName,
   532  		CharmName:            charmName,
   533  		TotalConnectedCount:  1,
   534  		ActiveConnectedCount: 1,
   535  	}, rel
   536  }
   537  
   538  var _ = gc.Suite(&allWatcherStateSuite{})
   539  
   540  type allWatcherStateSuite struct {
   541  	allWatcherBaseSuite
   542  }
   543  
   544  func (s *allWatcherStateSuite) reset(c *gc.C) {
   545  	s.TearDownTest(c)
   546  	s.SetUpTest(c)
   547  }
   548  
   549  func (s *allWatcherStateSuite) TestGetAll(c *gc.C) {
   550  	expectEntities := s.setUpScenario(c, s.state, 2)
   551  	s.checkGetAll(c, expectEntities)
   552  }
   553  
   554  func (s *allWatcherStateSuite) TestGetAllMultiModel(c *gc.C) {
   555  	// Set up 2 models and ensure that GetAll returns the
   556  	// entities for both models with no errors.
   557  	expectEntities := s.setUpScenario(c, s.state, 2)
   558  
   559  	// Use more units in the second model.
   560  	moreEntities := s.setUpScenario(c, s.newState(c), 4)
   561  
   562  	expectEntities = append(expectEntities, moreEntities...)
   563  
   564  	s.checkGetAll(c, expectEntities)
   565  }
   566  
   567  func (s *allWatcherStateSuite) checkGetAll(c *gc.C, expectEntities entityInfoSlice) {
   568  	b, err := NewAllWatcherBacking(s.pool)
   569  	c.Assert(err, jc.ErrorIsNil)
   570  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
   571  	err = b.GetAll(all)
   572  	c.Assert(err, jc.ErrorIsNil)
   573  	var gotEntities entityInfoSlice = all.All()
   574  	sort.Sort(gotEntities)
   575  	sort.Sort(expectEntities)
   576  	assertEntitiesEqual(c, gotEntities, expectEntities)
   577  }
   578  
   579  func applicationCharmURL(app *Application) *charm.URL {
   580  	urlStr, _ := app.CharmURL()
   581  	url := charm.MustParseURL(*urlStr)
   582  	return url
   583  }
   584  
   585  func setApplicationConfigAttr(c *gc.C, app *Application, attr string, val interface{}) {
   586  	err := app.UpdateCharmConfig(model.GenerationMaster, charm.Settings{attr: val})
   587  	c.Assert(err, jc.ErrorIsNil)
   588  }
   589  
   590  func setModelConfigAttr(c *gc.C, st *State, attr string, val interface{}) {
   591  	m, err := st.Model()
   592  	c.Assert(err, jc.ErrorIsNil)
   593  
   594  	err = m.UpdateModelConfig(map[string]interface{}{attr: val}, nil)
   595  	c.Assert(err, jc.ErrorIsNil)
   596  }
   597  
   598  // changeTestCase encapsulates entities to add, a change, and
   599  // the expected contents for a test.
   600  type changeTestCase struct {
   601  	// about describes the test case.
   602  	about string
   603  
   604  	// initialContents contains the infos of the
   605  	// watcher before signaling the change.
   606  	initialContents []multiwatcher.EntityInfo
   607  
   608  	// change signals the change of the watcher.
   609  	change watcher.Change
   610  
   611  	// expectContents contains the expected infos of
   612  	// the watcher after signaling the change.
   613  	expectContents []multiwatcher.EntityInfo
   614  }
   615  
   616  // changeTestFunc is a function for the preparation of a test and
   617  // the creation of the according case.
   618  type changeTestFunc func(c *gc.C, st *State) changeTestCase
   619  
   620  // performChangeTestCases runs a passed number of test cases for changes.
   621  func (s *allWatcherStateSuite) performChangeTestCases(c *gc.C, changeTestFuncs []changeTestFunc) {
   622  	for i, changeTestFunc := range changeTestFuncs {
   623  		test := changeTestFunc(c, s.state)
   624  
   625  		c.Logf("test %d. %s", i, test.about)
   626  		b, err := NewAllWatcherBacking(s.pool)
   627  		c.Assert(err, jc.ErrorIsNil)
   628  		all := multiwatcher.NewStore(loggo.GetLogger("test"))
   629  		for _, info := range test.initialContents {
   630  			all.Update(info)
   631  		}
   632  		err = b.Changed(all, test.change)
   633  		c.Assert(err, jc.ErrorIsNil)
   634  		entities := all.All()
   635  		assertEntitiesEqual(c, entities, test.expectContents)
   636  		s.reset(c)
   637  	}
   638  }
   639  
   640  func (s *allWatcherStateSuite) TestChangePermissions(c *gc.C) {
   641  	testChangePermissions(c, s.performChangeTestCases)
   642  }
   643  
   644  func (s *allWatcherStateSuite) TestChangeAnnotations(c *gc.C) {
   645  	testChangeAnnotations(c, s.performChangeTestCases)
   646  }
   647  
   648  func (s *allWatcherStateSuite) TestChangeMachines(c *gc.C) {
   649  	testChangeMachines(c, s.performChangeTestCases)
   650  }
   651  
   652  func (s *allWatcherStateSuite) TestChangeRelations(c *gc.C) {
   653  	testChangeRelations(c, s.owner, s.performChangeTestCases)
   654  }
   655  
   656  func (s *allWatcherStateSuite) TestChangeApplications(c *gc.C) {
   657  	testChangeApplications(c, s.owner, s.performChangeTestCases)
   658  }
   659  
   660  func strPtr(s string) *string {
   661  	return &s
   662  }
   663  
   664  func (s *allWatcherStateSuite) TestChangeCAASApplications(c *gc.C) {
   665  	loggo.GetLogger("juju.txn").SetLogLevel(loggo.TRACE)
   666  	changeTestFuncs := []changeTestFunc{
   667  		// Applications.
   668  		func(c *gc.C, st *State) changeTestCase {
   669  			return changeTestCase{
   670  				about: "not finding a podspec for a change is fine",
   671  				change: watcher.Change{
   672  					C:  "podSpecs",
   673  					Id: st.docID(applicationGlobalKey("mysql")),
   674  				}}
   675  		},
   676  		func(c *gc.C, st *State) changeTestCase {
   677  			caasSt := s.newCAASState(c)
   678  			m, err := caasSt.Model()
   679  			c.Assert(err, jc.ErrorIsNil)
   680  			cm, err := m.CAASModel()
   681  			c.Assert(err, jc.ErrorIsNil)
   682  			ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql")
   683  			mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch)
   684  			err = cm.SetPodSpec(nil, mysql.ApplicationTag(), strPtr("some podspec"))
   685  			c.Assert(err, jc.ErrorIsNil)
   686  			now := st.clock().Now()
   687  			return changeTestCase{
   688  				about: "initial CAAS application has podspec",
   689  				change: watcher.Change{
   690  					C:  "applications",
   691  					Id: caasSt.docID("mysql"),
   692  				},
   693  				expectContents: []multiwatcher.EntityInfo{
   694  					&multiwatcher.ApplicationInfo{
   695  						ModelUUID:   caasSt.ModelUUID(),
   696  						Name:        "mysql",
   697  						CharmURL:    "local:kubernetes/kubernetes-mysql-0",
   698  						Life:        "alive",
   699  						Config:      map[string]interface{}{},
   700  						Constraints: constraints.MustParse("arch=amd64"),
   701  						Status: multiwatcher.StatusInfo{
   702  							Current: "unset",
   703  							Data:    map[string]interface{}{},
   704  							Since:   &now,
   705  						},
   706  						OperatorStatus: multiwatcher.StatusInfo{
   707  							Current: "waiting",
   708  							Message: "waiting for container",
   709  							Data:    map[string]interface{}{},
   710  							Since:   &now,
   711  						},
   712  						PodSpec: &multiwatcher.PodSpec{
   713  							Spec: "some podspec",
   714  						},
   715  					},
   716  				},
   717  			}
   718  		},
   719  		func(c *gc.C, st *State) changeTestCase {
   720  			caasSt := s.newCAASState(c)
   721  			m, err := caasSt.Model()
   722  			c.Assert(err, jc.ErrorIsNil)
   723  			cm, err := m.CAASModel()
   724  			c.Assert(err, jc.ErrorIsNil)
   725  			ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql")
   726  			mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch)
   727  			err = cm.SetPodSpec(nil, mysql.ApplicationTag(), strPtr("some podspec"))
   728  			c.Assert(err, jc.ErrorIsNil)
   729  			return changeTestCase{
   730  				about: "application podspec is updated",
   731  				initialContents: []multiwatcher.EntityInfo{
   732  					&multiwatcher.ApplicationInfo{
   733  						ModelUUID: caasSt.ModelUUID(),
   734  						Name:      "mysql",
   735  					},
   736  				},
   737  				change: watcher.Change{
   738  					C:  "podSpecs",
   739  					Id: caasSt.docID(applicationGlobalKey("mysql")),
   740  				},
   741  				expectContents: []multiwatcher.EntityInfo{
   742  					&multiwatcher.ApplicationInfo{
   743  						ModelUUID: caasSt.ModelUUID(),
   744  						Name:      "mysql",
   745  						PodSpec: &multiwatcher.PodSpec{
   746  							Spec: "some podspec",
   747  						},
   748  					},
   749  				},
   750  			}
   751  		},
   752  		func(c *gc.C, st *State) changeTestCase {
   753  			caasSt := s.newCAASState(c)
   754  			ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql")
   755  			mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch)
   756  			now := st.clock().Now()
   757  			sInfo := status.StatusInfo{
   758  				Status:  status.Error,
   759  				Message: "failure",
   760  				Since:   &now,
   761  			}
   762  			err := mysql.SetOperatorStatus(sInfo)
   763  			c.Assert(err, jc.ErrorIsNil)
   764  			return changeTestCase{
   765  				about: "operator status update, updates application",
   766  				initialContents: []multiwatcher.EntityInfo{
   767  					&multiwatcher.ApplicationInfo{
   768  						ModelUUID: caasSt.ModelUUID(),
   769  						Name:      "mysql",
   770  					},
   771  				},
   772  				change: watcher.Change{
   773  					C:  "statuses",
   774  					Id: caasSt.docID(applicationGlobalOperatorKey("mysql")),
   775  				},
   776  				expectContents: []multiwatcher.EntityInfo{
   777  					&multiwatcher.ApplicationInfo{
   778  						ModelUUID: caasSt.ModelUUID(),
   779  						Name:      "mysql",
   780  						OperatorStatus: multiwatcher.StatusInfo{
   781  							Current: "error",
   782  							Message: "failure",
   783  							Data:    map[string]interface{}{},
   784  							Since:   &now,
   785  						},
   786  					},
   787  				},
   788  			}
   789  		},
   790  	}
   791  	s.performChangeTestCases(c, changeTestFuncs)
   792  }
   793  
   794  func (s *allWatcherStateSuite) TestChangeCAASUnits(c *gc.C) {
   795  	changeTestFuncs := []changeTestFunc{
   796  		func(c *gc.C, st *State) changeTestCase {
   797  			caasSt := s.newCAASState(c)
   798  			ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql")
   799  			mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch)
   800  			unit, err := mysql.AddUnit(AddUnitParams{})
   801  			c.Assert(err, jc.ErrorIsNil)
   802  
   803  			updateUnits := UpdateUnitsOperation{
   804  				Updates: []*UpdateUnitOperation{
   805  					unit.UpdateOperation(UnitUpdateProperties{
   806  						CloudContainerStatus: &status.StatusInfo{Status: status.Maintenance, Message: "setting up"},
   807  					}),
   808  				},
   809  			}
   810  			err = mysql.UpdateUnits(&updateUnits)
   811  			c.Assert(err, jc.ErrorIsNil)
   812  
   813  			now := st.clock().Now()
   814  			return changeTestCase{
   815  				about: "initial CAAS unit has container status",
   816  				change: watcher.Change{
   817  					C:  "units",
   818  					Id: caasSt.docID("mysql/0"),
   819  				},
   820  				expectContents: []multiwatcher.EntityInfo{
   821  					&multiwatcher.UnitInfo{
   822  						ModelUUID:   caasSt.ModelUUID(),
   823  						Name:        "mysql/0",
   824  						Application: "mysql",
   825  						Base:        "ubuntu@20.04",
   826  						Life:        "alive",
   827  						WorkloadStatus: multiwatcher.StatusInfo{
   828  							Current: "waiting",
   829  							Message: "installing agent",
   830  							Data:    map[string]interface{}{},
   831  							Since:   &now,
   832  						},
   833  						AgentStatus: multiwatcher.StatusInfo{
   834  							Current: "allocating",
   835  							Data:    map[string]interface{}{},
   836  							Since:   &now,
   837  						},
   838  						ContainerStatus: multiwatcher.StatusInfo{
   839  							Current: "maintenance",
   840  							Message: "setting up",
   841  							Data:    map[string]interface{}{},
   842  							Since:   &now,
   843  						},
   844  					},
   845  				},
   846  			}
   847  		},
   848  		func(c *gc.C, st *State) changeTestCase {
   849  			caasSt := s.newCAASState(c)
   850  			ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql")
   851  			mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch)
   852  			unit, err := mysql.AddUnit(AddUnitParams{})
   853  			c.Assert(err, jc.ErrorIsNil)
   854  
   855  			updateUnits := UpdateUnitsOperation{
   856  				Updates: []*UpdateUnitOperation{
   857  					unit.UpdateOperation(UnitUpdateProperties{
   858  						CloudContainerStatus: &status.StatusInfo{Status: status.Maintenance, Message: "setting up"},
   859  					}),
   860  				},
   861  			}
   862  			err = mysql.UpdateUnits(&updateUnits)
   863  			c.Assert(err, jc.ErrorIsNil)
   864  
   865  			now := st.clock().Now()
   866  			return changeTestCase{
   867  				about: "container status updates existing unit",
   868  				initialContents: []multiwatcher.EntityInfo{
   869  					&multiwatcher.UnitInfo{
   870  						ModelUUID:   caasSt.ModelUUID(),
   871  						Name:        "mysql/0",
   872  						Application: "mysql",
   873  						Base:        "ubuntu@20.04",
   874  					},
   875  				},
   876  				change: watcher.Change{
   877  					C:  "statuses",
   878  					Id: caasSt.docID(unit.globalCloudContainerKey()),
   879  				},
   880  				expectContents: []multiwatcher.EntityInfo{
   881  					&multiwatcher.UnitInfo{
   882  						ModelUUID:   caasSt.ModelUUID(),
   883  						Name:        "mysql/0",
   884  						Application: "mysql",
   885  						Base:        "ubuntu@20.04",
   886  						ContainerStatus: multiwatcher.StatusInfo{
   887  							Current: "maintenance",
   888  							Message: "setting up",
   889  							Data:    map[string]interface{}{},
   890  							Since:   &now,
   891  						},
   892  					},
   893  				},
   894  			}
   895  		},
   896  	}
   897  	s.performChangeTestCases(c, changeTestFuncs)
   898  }
   899  
   900  func (s *allWatcherStateSuite) TestChangeCharms(c *gc.C) {
   901  	testChangeCharms(c, s.owner, s.performChangeTestCases)
   902  }
   903  
   904  func (s *allWatcherStateSuite) TestChangeApplicationsConstraints(c *gc.C) {
   905  	testChangeApplicationsConstraints(c, s.owner, s.performChangeTestCases)
   906  }
   907  
   908  func (s *allWatcherStateSuite) TestChangeUnits(c *gc.C) {
   909  	testChangeUnits(c, s.owner, s.performChangeTestCases)
   910  }
   911  
   912  func (s *allWatcherStateSuite) TestChangeUnitsNonNilPorts(c *gc.C) {
   913  	testChangeUnitsNonNilPorts(c, s.owner, s.performChangeTestCases)
   914  }
   915  
   916  func (s *allWatcherStateSuite) TestChangeRemoteApplications(c *gc.C) {
   917  	testChangeRemoteApplications(c, s.performChangeTestCases)
   918  }
   919  
   920  func (s *allWatcherStateSuite) TestChangeApplicationOffers(c *gc.C) {
   921  	testChangeApplicationOffers(c, s.performChangeTestCases)
   922  }
   923  
   924  func (s *allWatcherStateSuite) TestChangeGenerations(c *gc.C) {
   925  	testChangeGenerations(c, s.performChangeTestCases)
   926  }
   927  
   928  func (s *allWatcherStateSuite) TestChangeActions(c *gc.C) {
   929  	changeTestFuncs := []changeTestFunc{
   930  		func(c *gc.C, st *State) changeTestCase {
   931  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
   932  			u, err := wordpress.AddUnit(AddUnitParams{})
   933  			c.Assert(err, jc.ErrorIsNil)
   934  			m, err := st.Model()
   935  			c.Assert(err, jc.ErrorIsNil)
   936  			operationID, err := m.EnqueueOperation("a test", 1)
   937  			c.Assert(err, jc.ErrorIsNil)
   938  			action, err := m.EnqueueAction(operationID, u.Tag(), "vacuumdb", map[string]interface{}{}, true, "group", nil)
   939  			c.Assert(err, jc.ErrorIsNil)
   940  			enqueued := makeActionInfo(action, st)
   941  			action, err = action.Begin()
   942  			c.Assert(err, jc.ErrorIsNil)
   943  			started := makeActionInfo(action, st)
   944  			return changeTestCase{
   945  				about:           "action change picks up last change",
   946  				initialContents: []multiwatcher.EntityInfo{&enqueued, &started},
   947  				change:          watcher.Change{C: actionsC, Id: st.docID(action.Id())},
   948  				expectContents:  []multiwatcher.EntityInfo{&started},
   949  			}
   950  		},
   951  	}
   952  	s.performChangeTestCases(c, changeTestFuncs)
   953  }
   954  
   955  func (s *allWatcherStateSuite) TestChangeBlocks(c *gc.C) {
   956  	changeTestFuncs := []changeTestFunc{
   957  		func(c *gc.C, st *State) changeTestCase {
   958  			return changeTestCase{
   959  				about: "no blocks in state, no blocks in store -> do nothing",
   960  				change: watcher.Change{
   961  					C:  blocksC,
   962  					Id: st.docID("1"),
   963  				}}
   964  		},
   965  		func(c *gc.C, st *State) changeTestCase {
   966  			blockId := st.docID("0")
   967  			blockType := DestroyBlock.ToParams()
   968  			blockMsg := "woot"
   969  			m, err := st.Model()
   970  			c.Assert(err, jc.ErrorIsNil)
   971  			return changeTestCase{
   972  				about: "no change if block is not in backing",
   973  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.BlockInfo{
   974  					ModelUUID: st.ModelUUID(),
   975  					ID:        blockId,
   976  					Type:      blockType,
   977  					Message:   blockMsg,
   978  					Tag:       m.ModelTag().String(),
   979  				}},
   980  				change: watcher.Change{
   981  					C:  blocksC,
   982  					Id: blockId,
   983  				},
   984  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.BlockInfo{
   985  					ModelUUID: st.ModelUUID(),
   986  					ID:        blockId,
   987  					Type:      blockType,
   988  					Message:   blockMsg,
   989  					Tag:       m.ModelTag().String(),
   990  				}},
   991  			}
   992  		},
   993  		func(c *gc.C, st *State) changeTestCase {
   994  			err := st.SwitchBlockOn(DestroyBlock, "multiwatcher testing")
   995  			c.Assert(err, jc.ErrorIsNil)
   996  			b, found, err := st.GetBlockForType(DestroyBlock)
   997  			c.Assert(err, jc.ErrorIsNil)
   998  			c.Assert(found, jc.IsTrue)
   999  			blockId := b.Id()
  1000  			m, err := st.Model()
  1001  			c.Assert(err, jc.ErrorIsNil)
  1002  			return changeTestCase{
  1003  				about: "block is added if it's in backing but not in Store",
  1004  				change: watcher.Change{
  1005  					C:  blocksC,
  1006  					Id: blockId,
  1007  				},
  1008  				expectContents: []multiwatcher.EntityInfo{
  1009  					&multiwatcher.BlockInfo{
  1010  						ModelUUID: st.ModelUUID(),
  1011  						ID:        st.localID(blockId),
  1012  						Type:      b.Type().ToParams(),
  1013  						Message:   b.Message(),
  1014  						Tag:       m.ModelTag().String(),
  1015  					}}}
  1016  		},
  1017  		func(c *gc.C, st *State) changeTestCase {
  1018  			err := st.SwitchBlockOn(DestroyBlock, "multiwatcher testing")
  1019  			c.Assert(err, jc.ErrorIsNil)
  1020  			b, found, err := st.GetBlockForType(DestroyBlock)
  1021  			c.Assert(err, jc.ErrorIsNil)
  1022  			c.Assert(found, jc.IsTrue)
  1023  			err = st.SwitchBlockOff(DestroyBlock)
  1024  			c.Assert(err, jc.ErrorIsNil)
  1025  
  1026  			return changeTestCase{
  1027  				about: "block is removed if it's in backing and in multiwatcher.Store",
  1028  				change: watcher.Change{
  1029  					C:  blocksC,
  1030  					Id: b.Id(),
  1031  				},
  1032  			}
  1033  		},
  1034  	}
  1035  	s.performChangeTestCases(c, changeTestFuncs)
  1036  }
  1037  
  1038  func (s *allWatcherStateSuite) TestClosingPorts(c *gc.C) {
  1039  	// Init the test model.
  1040  	wordpress := AddTestingApplication(c, s.state, "wordpress", AddTestingCharm(c, s.state, "wordpress"))
  1041  	u, err := wordpress.AddUnit(AddUnitParams{})
  1042  	c.Assert(err, jc.ErrorIsNil)
  1043  	m, err := s.state.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  1044  	c.Assert(err, jc.ErrorIsNil)
  1045  	err = u.AssignToMachine(m)
  1046  	c.Assert(err, jc.ErrorIsNil)
  1047  	publicAddress := network.NewSpaceAddress("1.2.3.4", network.WithScope(network.ScopePublic))
  1048  	privateAddress := network.NewSpaceAddress("4.3.2.1", network.WithScope(network.ScopeCloudLocal))
  1049  	MustOpenUnitPortRange(c, s.state, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp"))
  1050  	// Create all watcher state backing.
  1051  	b, err := NewAllWatcherBacking(s.pool)
  1052  	c.Assert(err, jc.ErrorIsNil)
  1053  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1054  	machineInfo := &multiwatcher.MachineInfo{
  1055  		ModelUUID:               s.state.ModelUUID(),
  1056  		ID:                      "0",
  1057  		PreferredPublicAddress:  publicAddress,
  1058  		PreferredPrivateAddress: privateAddress,
  1059  	}
  1060  	all.Update(machineInfo)
  1061  	// Check opened ports.
  1062  	err = b.Changed(all, watcher.Change{
  1063  		C:  "units",
  1064  		Id: s.state.docID("wordpress/0"),
  1065  	})
  1066  	c.Assert(err, jc.ErrorIsNil)
  1067  	entities := all.All()
  1068  	now := s.currentTime
  1069  	assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{
  1070  		&multiwatcher.UnitInfo{
  1071  			ModelUUID:      s.state.ModelUUID(),
  1072  			Name:           "wordpress/0",
  1073  			Application:    "wordpress",
  1074  			Base:           "ubuntu@12.10",
  1075  			Life:           life.Alive,
  1076  			MachineID:      "0",
  1077  			PublicAddress:  "1.2.3.4",
  1078  			PrivateAddress: "4.3.2.1",
  1079  			OpenPortRangesByEndpoint: corenetwork.GroupedPortRanges{
  1080  				allEndpoints: {corenetwork.MustParsePortRange("12345/tcp")},
  1081  			},
  1082  			WorkloadStatus: multiwatcher.StatusInfo{
  1083  				Current: "waiting",
  1084  				Message: "waiting for machine",
  1085  				Data:    map[string]interface{}{},
  1086  				Since:   &now,
  1087  			},
  1088  			AgentStatus: multiwatcher.StatusInfo{
  1089  				Current: "allocating",
  1090  				Data:    map[string]interface{}{},
  1091  				Since:   &now,
  1092  			},
  1093  		},
  1094  		machineInfo,
  1095  	})
  1096  	// Close the ports.
  1097  	MustCloseUnitPortRange(c, s.state, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp"))
  1098  	err = b.Changed(all, watcher.Change{
  1099  		C:  openedPortsC,
  1100  		Id: s.state.docID("0"),
  1101  	})
  1102  	c.Assert(err, jc.ErrorIsNil)
  1103  	entities = all.All()
  1104  	assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{
  1105  		&multiwatcher.UnitInfo{
  1106  			ModelUUID:      s.state.ModelUUID(),
  1107  			Name:           "wordpress/0",
  1108  			Application:    "wordpress",
  1109  			Base:           "ubuntu@12.10",
  1110  			MachineID:      "0",
  1111  			Life:           life.Alive,
  1112  			PublicAddress:  "1.2.3.4",
  1113  			PrivateAddress: "4.3.2.1",
  1114  			WorkloadStatus: multiwatcher.StatusInfo{
  1115  				Current: "waiting",
  1116  				Message: "waiting for machine",
  1117  				Data:    map[string]interface{}{},
  1118  				Since:   &now,
  1119  			},
  1120  			AgentStatus: multiwatcher.StatusInfo{
  1121  				Current: "allocating",
  1122  				Data:    map[string]interface{}{},
  1123  				Since:   &now,
  1124  			},
  1125  		},
  1126  		machineInfo,
  1127  	})
  1128  }
  1129  
  1130  func (s *allWatcherStateSuite) TestApplicationSettings(c *gc.C) {
  1131  	// Init the test model.
  1132  	app := AddTestingApplication(c, s.state, "dummy-application", AddTestingCharm(c, s.state, "dummy"))
  1133  	b, err := NewAllWatcherBacking(s.pool)
  1134  	c.Assert(err, jc.ErrorIsNil)
  1135  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1136  	// 1st scenario part: set settings and signal change.
  1137  	setApplicationConfigAttr(c, app, "username", "foo")
  1138  	setApplicationConfigAttr(c, app, "outlook", "foo@bar")
  1139  	all.Update(&multiwatcher.ApplicationInfo{
  1140  		ModelUUID: s.state.ModelUUID(),
  1141  		Name:      "dummy-application",
  1142  		CharmURL:  "local:quantal/quantal-dummy-1",
  1143  	})
  1144  	err = b.Changed(all, watcher.Change{
  1145  		C:  "settings",
  1146  		Id: s.state.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  1147  	})
  1148  	c.Assert(err, jc.ErrorIsNil)
  1149  	entities := all.All()
  1150  	assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{
  1151  		&multiwatcher.ApplicationInfo{
  1152  			ModelUUID: s.state.ModelUUID(),
  1153  			Name:      "dummy-application",
  1154  			CharmURL:  "local:quantal/quantal-dummy-1",
  1155  			Config:    charm.Settings{"outlook": "foo@bar", "username": "foo"},
  1156  		},
  1157  	})
  1158  	// 2nd scenario part: destroy the application and signal change.
  1159  	err = app.Destroy()
  1160  	c.Assert(err, jc.ErrorIsNil)
  1161  	err = b.Changed(all, watcher.Change{
  1162  		C:  "applications",
  1163  		Id: s.state.docID("dummy-application"),
  1164  	})
  1165  	c.Assert(err, jc.ErrorIsNil)
  1166  	entities = all.All()
  1167  	assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{})
  1168  }
  1169  
  1170  var _ = gc.Suite(&allModelWatcherStateSuite{})
  1171  
  1172  type allModelWatcherStateSuite struct {
  1173  	allWatcherBaseSuite
  1174  	state1 *State
  1175  }
  1176  
  1177  func (s *allModelWatcherStateSuite) SetUpTest(c *gc.C) {
  1178  	s.allWatcherBaseSuite.SetUpTest(c)
  1179  	s.state1 = s.newState(c)
  1180  }
  1181  
  1182  func (s *allModelWatcherStateSuite) Reset(c *gc.C) {
  1183  	s.TearDownTest(c)
  1184  	s.SetUpTest(c)
  1185  }
  1186  
  1187  func (s *allModelWatcherStateSuite) NewAllWatcherBacking() (AllWatcherBacking, error) {
  1188  	return NewAllWatcherBacking(s.pool)
  1189  }
  1190  
  1191  func (s *allModelWatcherStateSuite) TestMissingModelNotError(c *gc.C) {
  1192  	b, err := s.NewAllWatcherBacking()
  1193  	c.Assert(err, jc.ErrorIsNil)
  1194  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1195  
  1196  	dyingModel := "fake-uuid"
  1197  	st, err := s.pool.Get(dyingModel)
  1198  	c.Assert(err, jc.ErrorIsNil)
  1199  	defer st.Release()
  1200  
  1201  	removed, err := s.pool.Remove(dyingModel)
  1202  	c.Assert(err, jc.ErrorIsNil)
  1203  	c.Assert(removed, jc.IsFalse)
  1204  
  1205  	// If the state pool is in the process of removing a model, it will
  1206  	// return a NotFound error.
  1207  	err = b.Changed(all, watcher.Change{C: modelsC, Id: dyingModel})
  1208  	c.Assert(err, jc.ErrorIsNil)
  1209  }
  1210  
  1211  // performChangeTestCases runs a passed number of test cases for changes.
  1212  func (s *allModelWatcherStateSuite) performChangeTestCases(c *gc.C, changeTestFuncs []changeTestFunc) {
  1213  	for i, changeTestFunc := range changeTestFuncs {
  1214  		func() { // in aid of per-loop defers
  1215  			defer s.Reset(c)
  1216  
  1217  			test0 := changeTestFunc(c, s.state)
  1218  
  1219  			c.Logf("test %d. %s", i, test0.about)
  1220  			b, err := s.NewAllWatcherBacking()
  1221  			c.Assert(err, jc.ErrorIsNil)
  1222  			all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1223  
  1224  			// Do updates and check for first model.
  1225  			for _, info := range test0.initialContents {
  1226  				all.Update(info)
  1227  			}
  1228  			err = b.Changed(all, test0.change)
  1229  			c.Assert(err, jc.ErrorIsNil)
  1230  			var entities entityInfoSlice = all.All()
  1231  			assertEntitiesEqual(c, entities, test0.expectContents)
  1232  
  1233  			// Now do the same updates for a second model.
  1234  			test1 := changeTestFunc(c, s.state1)
  1235  			for _, info := range test1.initialContents {
  1236  				all.Update(info)
  1237  			}
  1238  			err = b.Changed(all, test1.change)
  1239  			c.Assert(err, jc.ErrorIsNil)
  1240  
  1241  			entities = all.All()
  1242  
  1243  			// Expected to see entities for both models.
  1244  			var expectedEntities entityInfoSlice = append(
  1245  				test0.expectContents,
  1246  				test1.expectContents...)
  1247  			sort.Sort(entities)
  1248  			sort.Sort(expectedEntities)
  1249  
  1250  			assertEntitiesEqual(c, entities, expectedEntities)
  1251  		}()
  1252  	}
  1253  }
  1254  
  1255  func (s *allModelWatcherStateSuite) TestChangePermissions(c *gc.C) {
  1256  	testChangePermissions(c, s.performChangeTestCases)
  1257  }
  1258  
  1259  func (s *allModelWatcherStateSuite) TestChangeAnnotations(c *gc.C) {
  1260  	testChangeAnnotations(c, s.performChangeTestCases)
  1261  }
  1262  
  1263  func (s *allModelWatcherStateSuite) TestChangeMachines(c *gc.C) {
  1264  	testChangeMachines(c, s.performChangeTestCases)
  1265  }
  1266  
  1267  func (s *allModelWatcherStateSuite) TestChangeRelations(c *gc.C) {
  1268  	testChangeRelations(c, s.owner, s.performChangeTestCases)
  1269  }
  1270  
  1271  func (s *allModelWatcherStateSuite) TestChangeApplications(c *gc.C) {
  1272  	testChangeApplications(c, s.owner, s.performChangeTestCases)
  1273  }
  1274  
  1275  func (s *allModelWatcherStateSuite) TestChangeApplicationsConstraints(c *gc.C) {
  1276  	testChangeApplicationsConstraints(c, s.owner, s.performChangeTestCases)
  1277  }
  1278  
  1279  func (s *allModelWatcherStateSuite) TestChangeUnits(c *gc.C) {
  1280  	testChangeUnits(c, s.owner, s.performChangeTestCases)
  1281  }
  1282  
  1283  func (s *allModelWatcherStateSuite) TestChangeUnitsNonNilPorts(c *gc.C) {
  1284  	testChangeUnitsNonNilPorts(c, s.owner, s.performChangeTestCases)
  1285  }
  1286  
  1287  func (s *allModelWatcherStateSuite) TestChangeRemoteApplications(c *gc.C) {
  1288  	testChangeRemoteApplications(c, s.performChangeTestCases)
  1289  }
  1290  
  1291  func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) {
  1292  	changeTestFuncs := []changeTestFunc{
  1293  		func(c *gc.C, st *State) changeTestCase {
  1294  			return changeTestCase{
  1295  				about: "no model in state -> do nothing",
  1296  				change: watcher.Change{
  1297  					C:  "models",
  1298  					Id: "non-existing-uuid",
  1299  				}}
  1300  		},
  1301  		func(c *gc.C, st *State) changeTestCase {
  1302  			return changeTestCase{
  1303  				about: "model is removed if it's not in backing",
  1304  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1305  					ModelUUID: "some-uuid",
  1306  				}},
  1307  				change: watcher.Change{
  1308  					C:  "models",
  1309  					Id: "some-uuid",
  1310  				}}
  1311  		},
  1312  		func(c *gc.C, st *State) changeTestCase {
  1313  			model, err := st.Model()
  1314  			c.Assert(err, jc.ErrorIsNil)
  1315  			err = model.SetSLA("essential", "test-sla-owner", nil)
  1316  			c.Assert(err, jc.ErrorIsNil)
  1317  			cfg, err := model.Config()
  1318  			c.Assert(err, jc.ErrorIsNil)
  1319  			status, err := model.Status()
  1320  			c.Assert(err, jc.ErrorIsNil)
  1321  			cons := constraints.MustParse("mem=4G")
  1322  			err = st.SetModelConstraints(cons)
  1323  			c.Assert(err, jc.ErrorIsNil)
  1324  			credential, _ := model.CloudCredentialTag()
  1325  
  1326  			return changeTestCase{
  1327  				about: "model is added if it's in backing but not in Store",
  1328  				change: watcher.Change{
  1329  					C:  "models",
  1330  					Id: st.ModelUUID(),
  1331  				},
  1332  				expectContents: []multiwatcher.EntityInfo{
  1333  					&multiwatcher.ModelInfo{
  1334  						ModelUUID:       model.UUID(),
  1335  						Type:            coremodel.ModelType(model.Type()),
  1336  						Name:            model.Name(),
  1337  						Life:            life.Alive,
  1338  						Owner:           model.Owner().Id(),
  1339  						ControllerUUID:  model.ControllerUUID(),
  1340  						IsController:    model.IsControllerModel(),
  1341  						Cloud:           model.CloudName(),
  1342  						CloudRegion:     model.CloudRegion(),
  1343  						CloudCredential: credential.Id(),
  1344  						Config:          cfg.AllAttrs(),
  1345  						Constraints:     cons,
  1346  						Status: multiwatcher.StatusInfo{
  1347  							Current: status.Status,
  1348  							Message: status.Message,
  1349  							Data:    status.Data,
  1350  							Since:   status.Since,
  1351  						},
  1352  						SLA: multiwatcher.ModelSLAInfo{
  1353  							Level: "essential",
  1354  							Owner: "test-sla-owner",
  1355  						},
  1356  						UserPermissions: map[string]permission.Access{
  1357  							"test-admin": permission.AdminAccess,
  1358  						},
  1359  					}}}
  1360  		},
  1361  		func(c *gc.C, st *State) changeTestCase {
  1362  			model, err := st.Model()
  1363  			c.Assert(err, jc.ErrorIsNil)
  1364  			cfg, err := model.Config()
  1365  			c.Assert(err, jc.ErrorIsNil)
  1366  			status, err := model.Status()
  1367  			c.Assert(err, jc.ErrorIsNil)
  1368  			credential, _ := model.CloudCredentialTag()
  1369  			return changeTestCase{
  1370  				about: "model is updated if it's in backing and in Store",
  1371  				initialContents: []multiwatcher.EntityInfo{
  1372  					&multiwatcher.ModelInfo{
  1373  						ModelUUID:      model.UUID(),
  1374  						Name:           "",
  1375  						Life:           life.Alive,
  1376  						Owner:          model.Owner().Id(),
  1377  						ControllerUUID: model.ControllerUUID(),
  1378  						IsController:   model.IsControllerModel(),
  1379  						Config:         cfg.AllAttrs(),
  1380  						Status: multiwatcher.StatusInfo{
  1381  							Current: status.Status,
  1382  							Message: status.Message,
  1383  							Data:    status.Data,
  1384  							Since:   status.Since,
  1385  						},
  1386  						SLA: multiwatcher.ModelSLAInfo{
  1387  							Level: "unsupported",
  1388  						},
  1389  						UserPermissions: map[string]permission.Access{
  1390  							"test-admin": permission.AdminAccess,
  1391  						},
  1392  					},
  1393  				},
  1394  				change: watcher.Change{
  1395  					C:  "models",
  1396  					Id: model.UUID(),
  1397  				},
  1398  				expectContents: []multiwatcher.EntityInfo{
  1399  					&multiwatcher.ModelInfo{
  1400  						ModelUUID:       model.UUID(),
  1401  						Type:            coremodel.ModelType(model.Type()),
  1402  						Name:            model.Name(),
  1403  						Life:            life.Alive,
  1404  						Owner:           model.Owner().Id(),
  1405  						ControllerUUID:  model.ControllerUUID(),
  1406  						IsController:    model.IsControllerModel(),
  1407  						Cloud:           model.CloudName(),
  1408  						CloudRegion:     model.CloudRegion(),
  1409  						CloudCredential: credential.Id(),
  1410  						Config:          cfg.AllAttrs(),
  1411  						Status: multiwatcher.StatusInfo{
  1412  							Current: status.Status,
  1413  							Message: status.Message,
  1414  							Data:    status.Data,
  1415  							Since:   status.Since,
  1416  						},
  1417  						SLA: multiwatcher.ModelSLAInfo{
  1418  							Level: "unsupported",
  1419  						},
  1420  						UserPermissions: map[string]permission.Access{
  1421  							"test-admin": permission.AdminAccess,
  1422  						},
  1423  					}}}
  1424  		},
  1425  		func(c *gc.C, st *State) changeTestCase {
  1426  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  1427  			err := app.SetConstraints(constraints.MustParse("mem=4G arch=amd64"))
  1428  			c.Assert(err, jc.ErrorIsNil)
  1429  
  1430  			return changeTestCase{
  1431  				about: "status is changed if the application exists in the store",
  1432  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  1433  					ModelUUID:   st.ModelUUID(),
  1434  					Name:        "wordpress",
  1435  					Constraints: constraints.MustParse("mem=99M cores=2 cpu-power=4"),
  1436  				}},
  1437  				change: watcher.Change{
  1438  					C:  "constraints",
  1439  					Id: st.docID("a#wordpress"),
  1440  				},
  1441  				expectContents: []multiwatcher.EntityInfo{
  1442  					&multiwatcher.ApplicationInfo{
  1443  						ModelUUID:   st.ModelUUID(),
  1444  						Name:        "wordpress",
  1445  						Constraints: constraints.MustParse("mem=4G arch=amd64"),
  1446  					}}}
  1447  		},
  1448  	}
  1449  	s.performChangeTestCases(c, changeTestFuncs)
  1450  }
  1451  
  1452  func (s *allModelWatcherStateSuite) TestChangeForDeadModel(c *gc.C) {
  1453  	// Ensure an entity is removed when a change is seen but
  1454  	// the model the entity belonged to has already died.
  1455  
  1456  	b, err := NewAllWatcherBacking(s.pool)
  1457  	c.Assert(err, jc.ErrorIsNil)
  1458  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1459  
  1460  	// Insert a machine for an model that doesn't actually
  1461  	// exist (mimics model removal).
  1462  	all.Update(&multiwatcher.MachineInfo{
  1463  		ModelUUID: "uuid",
  1464  		ID:        "0",
  1465  	})
  1466  	c.Assert(all.All(), gc.HasLen, 1)
  1467  
  1468  	err = b.Changed(all, watcher.Change{
  1469  		C:  "machines",
  1470  		Id: ensureModelUUID("uuid", "0"),
  1471  	})
  1472  	c.Assert(err, jc.ErrorIsNil)
  1473  
  1474  	// Entity info should be gone now.
  1475  	c.Assert(all.All(), gc.HasLen, 0)
  1476  }
  1477  
  1478  func (s *allModelWatcherStateSuite) TestModelSettings(c *gc.C) {
  1479  	// Init the test model.
  1480  	b, err := s.NewAllWatcherBacking()
  1481  	c.Assert(err, jc.ErrorIsNil)
  1482  	all := multiwatcher.NewStore(loggo.GetLogger("test"))
  1483  	setModelConfigAttr(c, s.state, "http-proxy", "http://invalid")
  1484  	setModelConfigAttr(c, s.state, "foo", "bar")
  1485  
  1486  	m, err := s.state.Model()
  1487  	c.Assert(err, jc.ErrorIsNil)
  1488  
  1489  	cfg, err := m.ModelConfig()
  1490  	c.Assert(err, jc.ErrorIsNil)
  1491  	expectedModelSettings := cfg.AllAttrs()
  1492  	expectedModelSettings["http-proxy"] = "http://invalid"
  1493  	expectedModelSettings["foo"] = "bar"
  1494  
  1495  	all.Update(&multiwatcher.ModelInfo{
  1496  		ModelUUID: s.state.ModelUUID(),
  1497  		Name:      "dummy-model",
  1498  	})
  1499  	err = b.Changed(all, watcher.Change{
  1500  		C:  "settings",
  1501  		Id: s.state.docID(modelGlobalKey),
  1502  	})
  1503  	c.Assert(err, jc.ErrorIsNil)
  1504  	entities := all.All()
  1505  	assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{
  1506  		&multiwatcher.ModelInfo{
  1507  			ModelUUID: s.state.ModelUUID(),
  1508  			Name:      "dummy-model",
  1509  			Config:    expectedModelSettings,
  1510  		},
  1511  	})
  1512  }
  1513  
  1514  // The testChange* funcs are extracted so the test cases can be used
  1515  // to test both the allWatcher and allModelWatcher.
  1516  
  1517  func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  1518  	changeTestFuncs := []changeTestFunc{
  1519  		func(c *gc.C, st *State) changeTestCase {
  1520  			model, err := st.Model()
  1521  			c.Assert(err, jc.ErrorIsNil)
  1522  			credential, _ := model.CloudCredentialTag()
  1523  
  1524  			return changeTestCase{
  1525  				about: "model update keeps permissions",
  1526  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1527  					ModelUUID: st.ModelUUID(),
  1528  					// Existence doesn't care about the other values, and they are
  1529  					// not entirely relevant to this test.
  1530  					UserPermissions: map[string]permission.Access{
  1531  						"bob":  permission.ReadAccess,
  1532  						"mary": permission.AdminAccess,
  1533  					},
  1534  				}},
  1535  				change: watcher.Change{
  1536  					C:  "models",
  1537  					Id: st.ModelUUID(),
  1538  				},
  1539  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1540  					ModelUUID:       st.ModelUUID(),
  1541  					Type:            coremodel.ModelType(model.Type()),
  1542  					Name:            model.Name(),
  1543  					Life:            "alive",
  1544  					Owner:           model.Owner().Id(),
  1545  					ControllerUUID:  testing.ControllerTag.Id(),
  1546  					IsController:    model.IsControllerModel(),
  1547  					Cloud:           model.CloudName(),
  1548  					CloudRegion:     model.CloudRegion(),
  1549  					CloudCredential: credential.Id(),
  1550  					SLA:             multiwatcher.ModelSLAInfo{Level: "unsupported"},
  1551  					UserPermissions: map[string]permission.Access{
  1552  						"bob":  permission.ReadAccess,
  1553  						"mary": permission.AdminAccess,
  1554  					},
  1555  				}}}
  1556  		},
  1557  
  1558  		func(c *gc.C, st *State) changeTestCase {
  1559  			model, err := st.Model()
  1560  			c.Assert(err, jc.ErrorIsNil)
  1561  
  1562  			_, err = model.AddUser(UserAccessSpec{
  1563  				User:      names.NewUserTag("tony@external"),
  1564  				CreatedBy: model.Owner(),
  1565  				Access:    permission.WriteAccess,
  1566  			})
  1567  			c.Assert(err, jc.ErrorIsNil)
  1568  
  1569  			return changeTestCase{
  1570  				about: "adding a model user updates model",
  1571  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1572  					ModelUUID: st.ModelUUID(),
  1573  					Name:      model.Name(),
  1574  					// Existence doesn't care about the other values, and they are
  1575  					// not entirely relevant to this test.
  1576  					UserPermissions: map[string]permission.Access{
  1577  						"bob":  permission.ReadAccess,
  1578  						"mary": permission.AdminAccess,
  1579  					},
  1580  				}},
  1581  				change: watcher.Change{
  1582  					C:  permissionsC,
  1583  					Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("tony@external")),
  1584  				},
  1585  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1586  					ModelUUID: st.ModelUUID(),
  1587  					Name:      model.Name(),
  1588  					// When the permissions are updated, only the user permissions are changed.
  1589  					UserPermissions: map[string]permission.Access{
  1590  						"bob":           permission.ReadAccess,
  1591  						"mary":          permission.AdminAccess,
  1592  						"tony@external": permission.WriteAccess,
  1593  					},
  1594  				}}}
  1595  		},
  1596  
  1597  		func(c *gc.C, st *State) changeTestCase {
  1598  			model, err := st.Model()
  1599  			c.Assert(err, jc.ErrorIsNil)
  1600  
  1601  			return changeTestCase{
  1602  				about: "removing a permission document removes user permission",
  1603  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1604  					ModelUUID: st.ModelUUID(),
  1605  					Name:      model.Name(),
  1606  					// Existence doesn't care about the other values, and they are
  1607  					// not entirely relevant to this test.
  1608  					UserPermissions: map[string]permission.Access{
  1609  						"bob":  permission.ReadAccess,
  1610  						"mary": permission.AdminAccess,
  1611  					},
  1612  				}},
  1613  				change: watcher.Change{
  1614  					C:     permissionsC,
  1615  					Id:    permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")),
  1616  					Revno: -1,
  1617  				},
  1618  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1619  					ModelUUID: st.ModelUUID(),
  1620  					Name:      model.Name(),
  1621  					// When the permissions are updated, only the user permissions are changed.
  1622  					UserPermissions: map[string]permission.Access{
  1623  						"mary": permission.AdminAccess,
  1624  					},
  1625  				}}}
  1626  		},
  1627  
  1628  		func(c *gc.C, st *State) changeTestCase {
  1629  			model, err := st.Model()
  1630  			c.Assert(err, jc.ErrorIsNil)
  1631  
  1632  			// With the allModelWatcher variant, this function is called twice
  1633  			// within the same test loop, so we look for bob, and if not found,
  1634  			// add him in.
  1635  			bob, err := st.User(names.NewUserTag("bob"))
  1636  			if errors.IsNotFound(err) {
  1637  				bob, err = st.AddUser("bob", "", "pwd", "admin")
  1638  			}
  1639  			c.Assert(err, jc.ErrorIsNil)
  1640  
  1641  			_, err = model.AddUser(UserAccessSpec{
  1642  				User:      bob.UserTag(),
  1643  				CreatedBy: model.Owner(),
  1644  				Access:    permission.WriteAccess,
  1645  			})
  1646  			c.Assert(err, jc.ErrorIsNil)
  1647  
  1648  			return changeTestCase{
  1649  				about: "updating a permission document updates user permission",
  1650  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1651  					ModelUUID: st.ModelUUID(),
  1652  					Name:      model.Name(),
  1653  					// Existence doesn't care about the other values, and they are
  1654  					// not entirely relevant to this test.
  1655  					UserPermissions: map[string]permission.Access{
  1656  						"bob":  permission.ReadAccess,
  1657  						"mary": permission.AdminAccess,
  1658  					},
  1659  				}},
  1660  				change: watcher.Change{
  1661  					C:  permissionsC,
  1662  					Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")),
  1663  				},
  1664  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{
  1665  					ModelUUID: st.ModelUUID(),
  1666  					Name:      model.Name(),
  1667  					UserPermissions: map[string]permission.Access{
  1668  						// Bob's permission updated to write.
  1669  						"bob":  permission.WriteAccess,
  1670  						"mary": permission.AdminAccess,
  1671  					},
  1672  				}}}
  1673  		},
  1674  	}
  1675  	runChangeTests(c, changeTestFuncs)
  1676  }
  1677  
  1678  func testChangeAnnotations(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  1679  	changeTestFuncs := []changeTestFunc{
  1680  		func(c *gc.C, st *State) changeTestCase {
  1681  			return changeTestCase{
  1682  				about: "no annotation in state, no annotation in store -> do nothing",
  1683  				change: watcher.Change{
  1684  					C:  "annotations",
  1685  					Id: st.docID("m#0"),
  1686  				}}
  1687  		},
  1688  		func(c *gc.C, st *State) changeTestCase {
  1689  			return changeTestCase{
  1690  				about: "annotation is removed if it's not in backing",
  1691  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.AnnotationInfo{
  1692  					ModelUUID: st.ModelUUID(),
  1693  					Tag:       "machine-0",
  1694  				}},
  1695  				change: watcher.Change{
  1696  					C:  "annotations",
  1697  					Id: st.docID("m#0"),
  1698  				}}
  1699  		},
  1700  		func(c *gc.C, st *State) changeTestCase {
  1701  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  1702  			c.Assert(err, jc.ErrorIsNil)
  1703  			model, err := st.Model()
  1704  			c.Assert(err, jc.ErrorIsNil)
  1705  			err = model.SetAnnotations(m, map[string]string{"foo": "bar", "arble": "baz"})
  1706  			c.Assert(err, jc.ErrorIsNil)
  1707  
  1708  			return changeTestCase{
  1709  				about: "annotation is added if it's in backing but not in Store",
  1710  				initialContents: []multiwatcher.EntityInfo{
  1711  					&multiwatcher.MachineInfo{
  1712  						ModelUUID: st.ModelUUID(),
  1713  						ID:        "0",
  1714  					},
  1715  				},
  1716  				change: watcher.Change{
  1717  					C:  "annotations",
  1718  					Id: st.docID("m#0"),
  1719  				},
  1720  				expectContents: []multiwatcher.EntityInfo{
  1721  					&multiwatcher.MachineInfo{
  1722  						ModelUUID:   st.ModelUUID(),
  1723  						ID:          "0",
  1724  						Annotations: map[string]string{"foo": "bar", "arble": "baz"},
  1725  					},
  1726  					&multiwatcher.AnnotationInfo{
  1727  						ModelUUID:   st.ModelUUID(),
  1728  						Tag:         "machine-0",
  1729  						Annotations: map[string]string{"foo": "bar", "arble": "baz"},
  1730  					}}}
  1731  		},
  1732  		func(c *gc.C, st *State) changeTestCase {
  1733  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  1734  			c.Assert(err, jc.ErrorIsNil)
  1735  			model, err := st.Model()
  1736  			c.Assert(err, jc.ErrorIsNil)
  1737  			err = model.SetAnnotations(m, map[string]string{
  1738  				"arble":  "khroomph",
  1739  				"pretty": "",
  1740  				"new":    "attr",
  1741  			})
  1742  			c.Assert(err, jc.ErrorIsNil)
  1743  
  1744  			return changeTestCase{
  1745  				about: "annotation is updated if it's in backing and in multiwatcher.Store",
  1746  				initialContents: []multiwatcher.EntityInfo{
  1747  					&multiwatcher.MachineInfo{
  1748  						ModelUUID: st.ModelUUID(),
  1749  						ID:        "0",
  1750  						Annotations: map[string]string{
  1751  							"arble":  "baz",
  1752  							"foo":    "bar",
  1753  							"pretty": "polly",
  1754  						}},
  1755  					&multiwatcher.AnnotationInfo{
  1756  						ModelUUID: st.ModelUUID(),
  1757  						Tag:       "machine-0",
  1758  						Annotations: map[string]string{
  1759  							"arble":  "baz",
  1760  							"foo":    "bar",
  1761  							"pretty": "polly",
  1762  						},
  1763  					}},
  1764  				change: watcher.Change{
  1765  					C:  "annotations",
  1766  					Id: st.docID("m#0"),
  1767  				},
  1768  				expectContents: []multiwatcher.EntityInfo{
  1769  					&multiwatcher.MachineInfo{
  1770  						ModelUUID: st.ModelUUID(),
  1771  						ID:        "0",
  1772  						Annotations: map[string]string{
  1773  							"arble": "khroomph",
  1774  							"new":   "attr",
  1775  						}},
  1776  					&multiwatcher.AnnotationInfo{
  1777  						ModelUUID: st.ModelUUID(),
  1778  						Tag:       "machine-0",
  1779  						Annotations: map[string]string{
  1780  							"arble": "khroomph",
  1781  							"new":   "attr",
  1782  						}}}}
  1783  		},
  1784  	}
  1785  	runChangeTests(c, changeTestFuncs)
  1786  }
  1787  
  1788  func testChangeMachines(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  1789  	changeTestFuncs := []changeTestFunc{
  1790  		func(c *gc.C, st *State) changeTestCase {
  1791  			return changeTestCase{
  1792  				about: "no machine in state -> do nothing",
  1793  				change: watcher.Change{
  1794  					C:  "statuses",
  1795  					Id: st.docID("m#0"),
  1796  				}}
  1797  		},
  1798  		func(c *gc.C, st *State) changeTestCase {
  1799  			return changeTestCase{
  1800  				about: "no machine in state, no machine in store -> do nothing",
  1801  				change: watcher.Change{
  1802  					C:  "machines",
  1803  					Id: st.docID("1"),
  1804  				}}
  1805  		},
  1806  		func(c *gc.C, st *State) changeTestCase {
  1807  			return changeTestCase{
  1808  				about: "machine is removed if it's not in backing",
  1809  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{
  1810  					ModelUUID: st.ModelUUID(),
  1811  					ID:        "1",
  1812  				}},
  1813  				change: watcher.Change{
  1814  					C:  "machines",
  1815  					Id: st.docID("1"),
  1816  				}}
  1817  		},
  1818  		func(c *gc.C, st *State) changeTestCase {
  1819  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  1820  			c.Assert(err, jc.ErrorIsNil)
  1821  			now := st.clock().Now()
  1822  			sInfo := status.StatusInfo{
  1823  				Status:  status.Error,
  1824  				Message: "failure",
  1825  				Since:   &now,
  1826  			}
  1827  			err = m.SetStatus(sInfo)
  1828  			c.Assert(err, jc.ErrorIsNil)
  1829  
  1830  			return changeTestCase{
  1831  				about: "machine is added if it's in backing but not in Store",
  1832  				change: watcher.Change{
  1833  					C:  "machines",
  1834  					Id: st.docID("0"),
  1835  				},
  1836  				expectContents: []multiwatcher.EntityInfo{
  1837  					&multiwatcher.MachineInfo{
  1838  						ModelUUID: st.ModelUUID(),
  1839  						ID:        "0",
  1840  						AgentStatus: multiwatcher.StatusInfo{
  1841  							Current: status.Error,
  1842  							Message: "failure",
  1843  							Data:    map[string]interface{}{},
  1844  							Since:   &now,
  1845  						},
  1846  						InstanceStatus: multiwatcher.StatusInfo{
  1847  							Current: status.Pending,
  1848  							Data:    map[string]interface{}{},
  1849  							Since:   &now,
  1850  						},
  1851  						Life:      life.Alive,
  1852  						Base:      "ubuntu@12.10",
  1853  						Jobs:      []coremodel.MachineJob{JobHostUnits.ToParams()},
  1854  						Addresses: []network.ProviderAddress{},
  1855  						HasVote:   false,
  1856  						WantsVote: false,
  1857  					}}}
  1858  		},
  1859  		func(c *gc.C, st *State) changeTestCase {
  1860  			m, err := st.AddMachine(UbuntuBase("22.04"), JobHostUnits)
  1861  			c.Assert(err, jc.ErrorIsNil)
  1862  			err = m.SetProvisioned("i-0", "", "bootstrap_nonce", nil)
  1863  			c.Assert(err, jc.ErrorIsNil)
  1864  			err = m.SetSupportedContainers([]instance.ContainerType{instance.LXD})
  1865  			c.Assert(err, jc.ErrorIsNil)
  1866  
  1867  			err = m.SetAgentVersion(version.MustParseBinary("2.4.1-ubuntu-amd64"))
  1868  			c.Assert(err, jc.ErrorIsNil)
  1869  			now := st.clock().Now()
  1870  			return changeTestCase{
  1871  				about: "machine is updated if it's in backing and in Store",
  1872  				initialContents: []multiwatcher.EntityInfo{
  1873  					&multiwatcher.MachineInfo{
  1874  						ModelUUID: st.ModelUUID(),
  1875  						ID:        "0",
  1876  						AgentStatus: multiwatcher.StatusInfo{
  1877  							Current: status.Error,
  1878  							Message: "another failure",
  1879  							Data:    map[string]interface{}{},
  1880  							Since:   &now,
  1881  						},
  1882  						InstanceStatus: multiwatcher.StatusInfo{
  1883  							Current: status.Pending,
  1884  							Data:    map[string]interface{}{},
  1885  							Since:   &now,
  1886  						},
  1887  					},
  1888  				},
  1889  				change: watcher.Change{
  1890  					C:  "machines",
  1891  					Id: st.docID("0"),
  1892  				},
  1893  				expectContents: []multiwatcher.EntityInfo{
  1894  					&multiwatcher.MachineInfo{
  1895  						ModelUUID:  st.ModelUUID(),
  1896  						ID:         "0",
  1897  						InstanceID: "i-0",
  1898  						AgentStatus: multiwatcher.StatusInfo{
  1899  							Current: status.Error,
  1900  							Message: "another failure",
  1901  							Data:    map[string]interface{}{},
  1902  							Version: "2.4.1",
  1903  							Since:   &now,
  1904  						},
  1905  						InstanceStatus: multiwatcher.StatusInfo{
  1906  							Current: status.Pending,
  1907  							Data:    map[string]interface{}{},
  1908  							Since:   &now,
  1909  						},
  1910  						Life:                     life.Alive,
  1911  						Base:                     "ubuntu@22.04",
  1912  						Jobs:                     []coremodel.MachineJob{JobHostUnits.ToParams()},
  1913  						Addresses:                []network.ProviderAddress{},
  1914  						HardwareCharacteristics:  &instance.HardwareCharacteristics{},
  1915  						CharmProfiles:            []string{},
  1916  						SupportedContainers:      []instance.ContainerType{instance.LXD},
  1917  						SupportedContainersKnown: true,
  1918  					}}}
  1919  		},
  1920  		func(c *gc.C, st *State) changeTestCase {
  1921  			now := st.clock().Now()
  1922  			return changeTestCase{
  1923  				about: "no change if status is not in backing",
  1924  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{
  1925  					ModelUUID: st.ModelUUID(),
  1926  					ID:        "0",
  1927  					AgentStatus: multiwatcher.StatusInfo{
  1928  						Current: status.Error,
  1929  						Message: "failure",
  1930  						Data:    map[string]interface{}{},
  1931  						Since:   &now,
  1932  					},
  1933  				}},
  1934  				change: watcher.Change{
  1935  					C:  "statuses",
  1936  					Id: st.docID("m#0"),
  1937  				},
  1938  				expectContents: []multiwatcher.EntityInfo{
  1939  					&multiwatcher.MachineInfo{
  1940  						ModelUUID: st.ModelUUID(),
  1941  						ID:        "0",
  1942  						AgentStatus: multiwatcher.StatusInfo{
  1943  							Current: status.Error,
  1944  							Message: "failure",
  1945  							Data:    map[string]interface{}{},
  1946  							Since:   &now,
  1947  						},
  1948  					}}}
  1949  		},
  1950  		func(c *gc.C, st *State) changeTestCase {
  1951  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  1952  			c.Assert(err, jc.ErrorIsNil)
  1953  			now := st.clock().Now()
  1954  			sInfo := status.StatusInfo{
  1955  				Status:  status.Started,
  1956  				Message: "",
  1957  				Since:   &now,
  1958  			}
  1959  			err = m.SetStatus(sInfo)
  1960  			c.Assert(err, jc.ErrorIsNil)
  1961  
  1962  			return changeTestCase{
  1963  				about: "status is changed if the machine exists in the store",
  1964  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{
  1965  					ModelUUID: st.ModelUUID(),
  1966  					ID:        "0",
  1967  					AgentStatus: multiwatcher.StatusInfo{
  1968  						Current: status.Error,
  1969  						Message: "failure",
  1970  						Data:    map[string]interface{}{},
  1971  						Since:   &now,
  1972  					},
  1973  				}},
  1974  				change: watcher.Change{
  1975  					C:  "statuses",
  1976  					Id: st.docID("m#0"),
  1977  				},
  1978  				expectContents: []multiwatcher.EntityInfo{
  1979  					&multiwatcher.MachineInfo{
  1980  						ModelUUID: st.ModelUUID(),
  1981  						ID:        "0",
  1982  						AgentStatus: multiwatcher.StatusInfo{
  1983  							Current: status.Started,
  1984  							Data:    make(map[string]interface{}),
  1985  							Since:   &now,
  1986  						},
  1987  					}}}
  1988  		},
  1989  		func(c *gc.C, st *State) changeTestCase {
  1990  			return changeTestCase{
  1991  				about: "no change if instanceData is not in backing",
  1992  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{
  1993  					ModelUUID: st.ModelUUID(),
  1994  					ID:        "0",
  1995  				}},
  1996  				change: watcher.Change{
  1997  					C:  "instanceData",
  1998  					Id: st.docID("0"),
  1999  				},
  2000  				expectContents: []multiwatcher.EntityInfo{
  2001  					&multiwatcher.MachineInfo{
  2002  						ModelUUID: st.ModelUUID(),
  2003  						ID:        "0",
  2004  					}}}
  2005  		},
  2006  		func(c *gc.C, st *State) changeTestCase {
  2007  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2008  			c.Assert(err, jc.ErrorIsNil)
  2009  
  2010  			hc := &instance.HardwareCharacteristics{}
  2011  			err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", hc)
  2012  			c.Assert(err, jc.ErrorIsNil)
  2013  
  2014  			profiles := []string{"default, juju-default"}
  2015  			err = m.SetCharmProfiles(profiles)
  2016  			c.Assert(err, jc.ErrorIsNil)
  2017  
  2018  			return changeTestCase{
  2019  				about: "instanceData is changed (CharmProfiles) if the machine exists in the store",
  2020  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{
  2021  					ModelUUID: st.ModelUUID(),
  2022  					ID:        "0",
  2023  				}},
  2024  				change: watcher.Change{
  2025  					C:  "instanceData",
  2026  					Id: st.docID("0"),
  2027  				},
  2028  				expectContents: []multiwatcher.EntityInfo{
  2029  					&multiwatcher.MachineInfo{
  2030  						ModelUUID:               st.ModelUUID(),
  2031  						ID:                      "0",
  2032  						CharmProfiles:           profiles,
  2033  						HardwareCharacteristics: hc,
  2034  					}}}
  2035  		},
  2036  	}
  2037  	runChangeTests(c, changeTestFuncs)
  2038  }
  2039  
  2040  func testChangeRelations(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  2041  	changeTestFuncs := []changeTestFunc{
  2042  		func(c *gc.C, st *State) changeTestCase {
  2043  			return changeTestCase{
  2044  				about: "no relation in state, no application in store -> do nothing",
  2045  				change: watcher.Change{
  2046  					C:  "relations",
  2047  					Id: st.docID("logging:logging-directory wordpress:logging-dir"),
  2048  				}}
  2049  		},
  2050  		func(c *gc.C, st *State) changeTestCase {
  2051  			return changeTestCase{
  2052  				about: "relation is removed if it's not in backing",
  2053  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.RelationInfo{
  2054  					ModelUUID: st.ModelUUID(),
  2055  					Key:       "logging:logging-directory wordpress:logging-dir",
  2056  				}},
  2057  				change: watcher.Change{
  2058  					C:  "relations",
  2059  					Id: st.docID("logging:logging-directory wordpress:logging-dir"),
  2060  				}}
  2061  		},
  2062  		func(c *gc.C, st *State) changeTestCase {
  2063  			AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2064  			AddTestingApplication(c, st, "logging", AddTestingCharm(c, st, "logging"))
  2065  			eps, err := st.InferEndpoints("logging", "wordpress")
  2066  			c.Assert(err, jc.ErrorIsNil)
  2067  			_, err = st.AddRelation(eps...)
  2068  			c.Assert(err, jc.ErrorIsNil)
  2069  
  2070  			return changeTestCase{
  2071  				about: "relation is added if it's in backing but not in Store",
  2072  				change: watcher.Change{
  2073  					C:  "relations",
  2074  					Id: st.docID("logging:logging-directory wordpress:logging-dir"),
  2075  				},
  2076  				expectContents: []multiwatcher.EntityInfo{
  2077  					&multiwatcher.RelationInfo{
  2078  						ModelUUID: st.ModelUUID(),
  2079  						Key:       "logging:logging-directory wordpress:logging-dir",
  2080  						Endpoints: []multiwatcher.Endpoint{
  2081  							{ApplicationName: "logging", Relation: multiwatcher.CharmRelation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}},
  2082  							{ApplicationName: "wordpress", Relation: multiwatcher.CharmRelation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}},
  2083  					}}}
  2084  		},
  2085  	}
  2086  	runChangeTests(c, changeTestFuncs)
  2087  }
  2088  
  2089  func testChangeApplications(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  2090  	// TODO(wallyworld) - add test for changing application status when that is implemented
  2091  	changeTestFuncs := []changeTestFunc{
  2092  		// Applications.
  2093  		func(c *gc.C, st *State) changeTestCase {
  2094  			return changeTestCase{
  2095  				about: "no application in state, no application in store -> do nothing",
  2096  				change: watcher.Change{
  2097  					C:  "applications",
  2098  					Id: st.docID("wordpress"),
  2099  				}}
  2100  		},
  2101  		func(c *gc.C, st *State) changeTestCase {
  2102  			return changeTestCase{
  2103  				about: "application is removed if it's not in backing",
  2104  				initialContents: []multiwatcher.EntityInfo{
  2105  					&multiwatcher.ApplicationInfo{
  2106  						ModelUUID: st.ModelUUID(),
  2107  						Name:      "wordpress",
  2108  					},
  2109  				},
  2110  				change: watcher.Change{
  2111  					C:  "applications",
  2112  					Id: st.docID("wordpress"),
  2113  				}}
  2114  		},
  2115  		func(c *gc.C, st *State) changeTestCase {
  2116  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2117  			err := wordpress.MergeExposeSettings(nil)
  2118  			c.Assert(err, jc.ErrorIsNil)
  2119  			err = wordpress.SetMinUnits(42)
  2120  			c.Assert(err, jc.ErrorIsNil)
  2121  			now := st.clock().Now()
  2122  			return changeTestCase{
  2123  				about: "application is added if it's in backing but not in Store",
  2124  				change: watcher.Change{
  2125  					C:  "applications",
  2126  					Id: st.docID("wordpress"),
  2127  				},
  2128  				expectContents: []multiwatcher.EntityInfo{
  2129  					&multiwatcher.ApplicationInfo{
  2130  						ModelUUID:   st.ModelUUID(),
  2131  						Name:        "wordpress",
  2132  						Exposed:     true,
  2133  						CharmURL:    "local:quantal/quantal-wordpress-3",
  2134  						Life:        life.Alive,
  2135  						MinUnits:    42,
  2136  						Config:      charm.Settings{},
  2137  						Constraints: constraints.MustParse("arch=amd64"),
  2138  						Status: multiwatcher.StatusInfo{
  2139  							Current: "unset",
  2140  							Message: "",
  2141  							Data:    map[string]interface{}{},
  2142  							Since:   &now,
  2143  						},
  2144  					}}}
  2145  		},
  2146  		func(c *gc.C, st *State) changeTestCase {
  2147  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2148  			setApplicationConfigAttr(c, app, "blog-title", "boring")
  2149  
  2150  			return changeTestCase{
  2151  				about: "application is updated if it's in backing and in multiwatcher.Store",
  2152  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2153  					ModelUUID:   st.ModelUUID(),
  2154  					Name:        "wordpress",
  2155  					Exposed:     true,
  2156  					CharmURL:    "local:quantal/quantal-wordpress-3",
  2157  					MinUnits:    47,
  2158  					Constraints: constraints.MustParse("mem=99M"),
  2159  					Config:      charm.Settings{"blog-title": "boring"},
  2160  				}},
  2161  				change: watcher.Change{
  2162  					C:  "applications",
  2163  					Id: st.docID("wordpress"),
  2164  				},
  2165  				expectContents: []multiwatcher.EntityInfo{
  2166  					&multiwatcher.ApplicationInfo{
  2167  						ModelUUID:   st.ModelUUID(),
  2168  						Name:        "wordpress",
  2169  						CharmURL:    "local:quantal/quantal-wordpress-3",
  2170  						Life:        life.Alive,
  2171  						Constraints: constraints.MustParse("mem=99M"),
  2172  						Config:      charm.Settings{"blog-title": "boring"},
  2173  					}}}
  2174  		},
  2175  		func(c *gc.C, st *State) changeTestCase {
  2176  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2177  			unit, err := app.AddUnit(AddUnitParams{})
  2178  			c.Assert(err, jc.ErrorIsNil)
  2179  			err = unit.SetWorkloadVersion("42.47")
  2180  			c.Assert(err, jc.ErrorIsNil)
  2181  			return changeTestCase{
  2182  				about: "workload version is updated when set on a unit",
  2183  				initialContents: []multiwatcher.EntityInfo{
  2184  					&multiwatcher.UnitInfo{
  2185  						ModelUUID:   st.ModelUUID(),
  2186  						Name:        "wordpress/0",
  2187  						Application: "wordpress",
  2188  					},
  2189  					&multiwatcher.ApplicationInfo{
  2190  						ModelUUID: st.ModelUUID(),
  2191  						Name:      "wordpress",
  2192  						CharmURL:  "local:quantal/quantal-wordpress-3",
  2193  					},
  2194  				},
  2195  				change: watcher.Change{
  2196  					C:  "statuses",
  2197  					Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"),
  2198  				},
  2199  				expectContents: []multiwatcher.EntityInfo{
  2200  					&multiwatcher.ApplicationInfo{
  2201  						ModelUUID:       st.ModelUUID(),
  2202  						Name:            "wordpress",
  2203  						CharmURL:        "local:quantal/quantal-wordpress-3",
  2204  						WorkloadVersion: "42.47",
  2205  					},
  2206  					&multiwatcher.UnitInfo{
  2207  						ModelUUID:   st.ModelUUID(),
  2208  						Name:        "wordpress/0",
  2209  						Application: "wordpress",
  2210  					},
  2211  				},
  2212  			}
  2213  		},
  2214  		func(c *gc.C, st *State) changeTestCase {
  2215  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2216  			unit, err := app.AddUnit(AddUnitParams{})
  2217  			c.Assert(err, jc.ErrorIsNil)
  2218  			err = unit.SetWorkloadVersion("")
  2219  			c.Assert(err, jc.ErrorIsNil)
  2220  			return changeTestCase{
  2221  				about: "workload version is not updated when empty",
  2222  				initialContents: []multiwatcher.EntityInfo{
  2223  					&multiwatcher.UnitInfo{
  2224  						ModelUUID:   st.ModelUUID(),
  2225  						Name:        "wordpress/0",
  2226  						Application: "wordpress",
  2227  					},
  2228  					&multiwatcher.ApplicationInfo{
  2229  						ModelUUID:       st.ModelUUID(),
  2230  						Name:            "wordpress",
  2231  						CharmURL:        "local:quantal/quantal-wordpress-3",
  2232  						WorkloadVersion: "ultimate",
  2233  					},
  2234  				},
  2235  				change: watcher.Change{
  2236  					C:  "statuses",
  2237  					Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"),
  2238  				},
  2239  				expectContents: []multiwatcher.EntityInfo{
  2240  					&multiwatcher.ApplicationInfo{
  2241  						ModelUUID:       st.ModelUUID(),
  2242  						Name:            "wordpress",
  2243  						CharmURL:        "local:quantal/quantal-wordpress-3",
  2244  						WorkloadVersion: "ultimate",
  2245  					},
  2246  					&multiwatcher.UnitInfo{
  2247  						ModelUUID:   st.ModelUUID(),
  2248  						Name:        "wordpress/0",
  2249  						Application: "wordpress",
  2250  					},
  2251  				},
  2252  			}
  2253  		},
  2254  		func(c *gc.C, st *State) changeTestCase {
  2255  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2256  			unit, err := app.AddUnit(AddUnitParams{})
  2257  			c.Assert(err, jc.ErrorIsNil)
  2258  			err = unit.SetWorkloadVersion("42.47")
  2259  			c.Assert(err, jc.ErrorIsNil)
  2260  			return changeTestCase{
  2261  				about: "workload version is ignored if there is no application info",
  2262  				initialContents: []multiwatcher.EntityInfo{
  2263  					&multiwatcher.UnitInfo{
  2264  						ModelUUID:   st.ModelUUID(),
  2265  						Name:        "wordpress/0",
  2266  						Application: "wordpress",
  2267  					},
  2268  				},
  2269  				change: watcher.Change{
  2270  					C:  "statuses",
  2271  					Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"),
  2272  				},
  2273  				expectContents: []multiwatcher.EntityInfo{
  2274  					&multiwatcher.UnitInfo{
  2275  						ModelUUID:   st.ModelUUID(),
  2276  						Name:        "wordpress/0",
  2277  						Application: "wordpress",
  2278  					},
  2279  				},
  2280  			}
  2281  		},
  2282  		func(c *gc.C, st *State) changeTestCase {
  2283  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2284  			setApplicationConfigAttr(c, app, "blog-title", "boring")
  2285  
  2286  			return changeTestCase{
  2287  				about: "application re-reads config when charm URL changes",
  2288  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2289  					ModelUUID: st.ModelUUID(),
  2290  					Name:      "wordpress",
  2291  					// Note: CharmURL has a different revision number from
  2292  					// the wordpress revision in the testing repo.
  2293  					CharmURL: "local:quantal/quantal-wordpress-2",
  2294  					Config:   charm.Settings{"foo": "bar"},
  2295  				}},
  2296  				change: watcher.Change{
  2297  					C:  "applications",
  2298  					Id: st.docID("wordpress"),
  2299  				},
  2300  				expectContents: []multiwatcher.EntityInfo{
  2301  					&multiwatcher.ApplicationInfo{
  2302  						ModelUUID: st.ModelUUID(),
  2303  						Name:      "wordpress",
  2304  						CharmURL:  "local:quantal/quantal-wordpress-3",
  2305  						Life:      life.Alive,
  2306  						Config:    charm.Settings{"blog-title": "boring"},
  2307  					}}}
  2308  		},
  2309  		// Settings.
  2310  		func(c *gc.C, st *State) changeTestCase {
  2311  			return changeTestCase{
  2312  				about: "no application in state -> do nothing",
  2313  				change: watcher.Change{
  2314  					C:  "settings",
  2315  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2316  				}}
  2317  		},
  2318  		func(c *gc.C, st *State) changeTestCase {
  2319  			return changeTestCase{
  2320  				about: "no change if application is not in backing",
  2321  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2322  					ModelUUID: st.ModelUUID(),
  2323  					Name:      "dummy-application",
  2324  					CharmURL:  "local:quantal/quantal-dummy-1",
  2325  				}},
  2326  				change: watcher.Change{
  2327  					C:  "settings",
  2328  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2329  				},
  2330  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2331  					ModelUUID: st.ModelUUID(),
  2332  					Name:      "dummy-application",
  2333  					CharmURL:  "local:quantal/quantal-dummy-1",
  2334  				}}}
  2335  		},
  2336  		func(c *gc.C, st *State) changeTestCase {
  2337  			app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy"))
  2338  			setApplicationConfigAttr(c, app, "username", "foo")
  2339  			setApplicationConfigAttr(c, app, "outlook", "foo@bar")
  2340  
  2341  			return changeTestCase{
  2342  				about: "application config is changed if application exists in the store with the same URL",
  2343  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2344  					ModelUUID: st.ModelUUID(),
  2345  					Name:      "dummy-application",
  2346  					CharmURL:  "local:quantal/quantal-dummy-1",
  2347  				}},
  2348  				change: watcher.Change{
  2349  					C:  "settings",
  2350  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2351  				},
  2352  				expectContents: []multiwatcher.EntityInfo{
  2353  					&multiwatcher.ApplicationInfo{
  2354  						ModelUUID: st.ModelUUID(),
  2355  						Name:      "dummy-application",
  2356  						CharmURL:  "local:quantal/quantal-dummy-1",
  2357  						Config:    charm.Settings{"username": "foo", "outlook": "foo@bar"},
  2358  					}}}
  2359  		},
  2360  		func(c *gc.C, st *State) changeTestCase {
  2361  			app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy"))
  2362  			setApplicationConfigAttr(c, app, "username", "foo")
  2363  			setApplicationConfigAttr(c, app, "outlook", "foo@bar")
  2364  			setApplicationConfigAttr(c, app, "username", nil)
  2365  
  2366  			return changeTestCase{
  2367  				about: "application config is changed after removing of a setting",
  2368  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2369  					ModelUUID: st.ModelUUID(),
  2370  					Name:      "dummy-application",
  2371  					CharmURL:  "local:quantal/quantal-dummy-1",
  2372  					Config:    charm.Settings{"username": "foo", "outlook": "foo@bar"},
  2373  				}},
  2374  				change: watcher.Change{
  2375  					C:  "settings",
  2376  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2377  				},
  2378  				expectContents: []multiwatcher.EntityInfo{
  2379  					&multiwatcher.ApplicationInfo{
  2380  						ModelUUID: st.ModelUUID(),
  2381  						Name:      "dummy-application",
  2382  						CharmURL:  "local:quantal/quantal-dummy-1",
  2383  						Config:    charm.Settings{"outlook": "foo@bar"},
  2384  					}}}
  2385  		},
  2386  		func(c *gc.C, st *State) changeTestCase {
  2387  			testCharm := AddCustomCharm(
  2388  				c, st, "dummy",
  2389  				"config.yaml", dottedConfig,
  2390  				"quantal", 1)
  2391  			app := AddTestingApplication(c, st, "dummy-application", testCharm)
  2392  			setApplicationConfigAttr(c, app, "key.dotted", "foo")
  2393  
  2394  			return changeTestCase{
  2395  				about: "application config is unescaped when reading from the backing store",
  2396  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2397  					ModelUUID: st.ModelUUID(),
  2398  					Name:      "dummy-application",
  2399  					CharmURL:  "local:quantal/quantal-dummy-1",
  2400  					Config:    charm.Settings{"key.dotted": "bar"},
  2401  				}},
  2402  				change: watcher.Change{
  2403  					C:  "settings",
  2404  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2405  				},
  2406  				expectContents: []multiwatcher.EntityInfo{
  2407  					&multiwatcher.ApplicationInfo{
  2408  						ModelUUID: st.ModelUUID(),
  2409  						Name:      "dummy-application",
  2410  						CharmURL:  "local:quantal/quantal-dummy-1",
  2411  						Config:    charm.Settings{"key.dotted": "foo"},
  2412  					}}}
  2413  		},
  2414  		func(c *gc.C, st *State) changeTestCase {
  2415  			app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy"))
  2416  			setApplicationConfigAttr(c, app, "username", "foo")
  2417  
  2418  			return changeTestCase{
  2419  				about: "application config is unchanged if application exists in the store with a different URL",
  2420  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2421  					ModelUUID: st.ModelUUID(),
  2422  					Name:      "dummy-application",
  2423  					CharmURL:  "local:quantal/quantal-dummy-2", // Note different revno.
  2424  					Config:    charm.Settings{"username": "bar"},
  2425  				}},
  2426  				change: watcher.Change{
  2427  					C:  "settings",
  2428  					Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"),
  2429  				},
  2430  				expectContents: []multiwatcher.EntityInfo{
  2431  					&multiwatcher.ApplicationInfo{
  2432  						ModelUUID: st.ModelUUID(),
  2433  						Name:      "dummy-application",
  2434  						CharmURL:  "local:quantal/quantal-dummy-2",
  2435  						Config:    charm.Settings{"username": "bar"},
  2436  					}}}
  2437  		},
  2438  		func(c *gc.C, st *State) changeTestCase {
  2439  			return changeTestCase{
  2440  				about: "non-application config change is ignored",
  2441  				change: watcher.Change{
  2442  					C:  "settings",
  2443  					Id: st.docID("m#0"),
  2444  				}}
  2445  		},
  2446  		func(c *gc.C, st *State) changeTestCase {
  2447  			return changeTestCase{
  2448  				about: "application config change with no charm url is ignored",
  2449  				change: watcher.Change{
  2450  					C:  "settings",
  2451  					Id: st.docID("a#foo"),
  2452  				}}
  2453  		},
  2454  	}
  2455  	runChangeTests(c, changeTestFuncs)
  2456  }
  2457  
  2458  func testChangeCharms(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  2459  	changeTestFuncs := []changeTestFunc{
  2460  		func(c *gc.C, st *State) changeTestCase {
  2461  			return changeTestCase{
  2462  				about: "no charm in state, no charm in store -> do nothing",
  2463  				change: watcher.Change{
  2464  					C:  "charms",
  2465  					Id: st.docID("wordpress"),
  2466  				}}
  2467  		},
  2468  		func(c *gc.C, st *State) changeTestCase {
  2469  			return changeTestCase{
  2470  				about: "charm is removed if it's not in backing",
  2471  				initialContents: []multiwatcher.EntityInfo{
  2472  					&multiwatcher.CharmInfo{
  2473  						ModelUUID: st.ModelUUID(),
  2474  						CharmURL:  "local:quantal/quantal-wordpress-2",
  2475  					},
  2476  				},
  2477  				change: watcher.Change{
  2478  					C:  "charms",
  2479  					Id: st.docID("local:quantal/quantal-wordpress-2"),
  2480  				}}
  2481  		},
  2482  		func(c *gc.C, st *State) changeTestCase {
  2483  			ch := AddTestingCharm(c, st, "wordpress")
  2484  			return changeTestCase{
  2485  				about: "charm is added if it's in backing but not in Store",
  2486  				change: watcher.Change{
  2487  					C:  "charms",
  2488  					Id: st.docID(ch.URL()),
  2489  				},
  2490  				expectContents: []multiwatcher.EntityInfo{
  2491  					&multiwatcher.CharmInfo{
  2492  						ModelUUID:     st.ModelUUID(),
  2493  						CharmURL:      ch.URL(),
  2494  						Life:          life.Alive,
  2495  						DefaultConfig: map[string]interface{}{"blog-title": "My Title"},
  2496  					}}}
  2497  		},
  2498  	}
  2499  	runChangeTests(c, changeTestFuncs)
  2500  }
  2501  
  2502  func testChangeApplicationsConstraints(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  2503  	changeTestFuncs := []changeTestFunc{
  2504  		func(c *gc.C, st *State) changeTestCase {
  2505  			return changeTestCase{
  2506  				about: "no application in state -> do nothing",
  2507  				change: watcher.Change{
  2508  					C:  "constraints",
  2509  					Id: st.docID("a#wordpress"),
  2510  				}}
  2511  		},
  2512  		func(c *gc.C, st *State) changeTestCase {
  2513  			return changeTestCase{
  2514  				about: "no change if application is not in backing",
  2515  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2516  					ModelUUID:   st.ModelUUID(),
  2517  					Name:        "wordpress",
  2518  					Constraints: constraints.MustParse("mem=99M"),
  2519  				}},
  2520  				change: watcher.Change{
  2521  					C:  "constraints",
  2522  					Id: st.docID("a#wordpress"),
  2523  				},
  2524  				expectContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2525  					ModelUUID:   st.ModelUUID(),
  2526  					Name:        "wordpress",
  2527  					Constraints: constraints.MustParse("mem=99M"),
  2528  				}}}
  2529  		},
  2530  		func(c *gc.C, st *State) changeTestCase {
  2531  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2532  			err := app.SetConstraints(constraints.MustParse("mem=4G arch=amd64"))
  2533  			c.Assert(err, jc.ErrorIsNil)
  2534  
  2535  			return changeTestCase{
  2536  				about: "status is changed if the application exists in the store",
  2537  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{
  2538  					ModelUUID:   st.ModelUUID(),
  2539  					Name:        "wordpress",
  2540  					Constraints: constraints.MustParse("mem=99M cores=2 cpu-power=4"),
  2541  				}},
  2542  				change: watcher.Change{
  2543  					C:  "constraints",
  2544  					Id: st.docID("a#wordpress"),
  2545  				},
  2546  				expectContents: []multiwatcher.EntityInfo{
  2547  					&multiwatcher.ApplicationInfo{
  2548  						ModelUUID:   st.ModelUUID(),
  2549  						Name:        "wordpress",
  2550  						Constraints: constraints.MustParse("mem=4G arch=amd64"),
  2551  					}}}
  2552  		},
  2553  	}
  2554  	runChangeTests(c, changeTestFuncs)
  2555  }
  2556  
  2557  func testChangeUnits(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  2558  	changeTestFuncs := []changeTestFunc{
  2559  		func(c *gc.C, st *State) changeTestCase {
  2560  			return changeTestCase{
  2561  				about: "no unit in state, no unit in store -> do nothing",
  2562  				change: watcher.Change{
  2563  					C:  "units",
  2564  					Id: st.docID("1"),
  2565  				}}
  2566  		},
  2567  		func(c *gc.C, st *State) changeTestCase {
  2568  			return changeTestCase{
  2569  				about: "unit is removed if it's not in backing",
  2570  				initialContents: []multiwatcher.EntityInfo{
  2571  					&multiwatcher.UnitInfo{
  2572  						ModelUUID: st.ModelUUID(),
  2573  						Name:      "wordpress/1",
  2574  						Life:      life.Alive,
  2575  					},
  2576  				},
  2577  				change: watcher.Change{
  2578  					C:  "units",
  2579  					Id: st.docID("wordpress/1"),
  2580  				}}
  2581  		},
  2582  		func(c *gc.C, st *State) changeTestCase {
  2583  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2584  			u, err := wordpress.AddUnit(AddUnitParams{})
  2585  			c.Assert(err, jc.ErrorIsNil)
  2586  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2587  			c.Assert(err, jc.ErrorIsNil)
  2588  			err = u.AssignToMachine(m)
  2589  			c.Assert(err, jc.ErrorIsNil)
  2590  			MustOpenUnitPortRanges(c, u.st, m, u.Name(), allEndpoints, []corenetwork.PortRange{
  2591  				corenetwork.MustParsePortRange("12345/tcp"),
  2592  				corenetwork.MustParsePortRange("54321/udp"),
  2593  				corenetwork.MustParsePortRange("5555-5558/tcp"),
  2594  			})
  2595  			c.Assert(err, jc.ErrorIsNil)
  2596  			now := st.clock().Now()
  2597  			sInfo := status.StatusInfo{
  2598  				Status:  status.Error,
  2599  				Message: "failure",
  2600  				Since:   &now,
  2601  			}
  2602  			err = u.SetAgentStatus(sInfo)
  2603  			c.Assert(err, jc.ErrorIsNil)
  2604  
  2605  			return changeTestCase{
  2606  				about: "unit is added if it's in backing but not in Store",
  2607  				change: watcher.Change{
  2608  					C:  "units",
  2609  					Id: st.docID("wordpress/0"),
  2610  				},
  2611  				expectContents: []multiwatcher.EntityInfo{
  2612  					&multiwatcher.UnitInfo{
  2613  						ModelUUID:   st.ModelUUID(),
  2614  						Name:        "wordpress/0",
  2615  						Application: "wordpress",
  2616  						Base:        "ubuntu@12.10",
  2617  						Life:        life.Alive,
  2618  						MachineID:   "0",
  2619  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2620  							allEndpoints: {
  2621  								corenetwork.MustParsePortRange("5555-5558/tcp"),
  2622  								corenetwork.MustParsePortRange("12345/tcp"),
  2623  								corenetwork.MustParsePortRange("54321/udp"),
  2624  							},
  2625  						},
  2626  						AgentStatus: multiwatcher.StatusInfo{
  2627  							Current: "idle",
  2628  							Message: "",
  2629  							Data:    map[string]interface{}{},
  2630  							Since:   &now,
  2631  						},
  2632  						WorkloadStatus: multiwatcher.StatusInfo{
  2633  							Current: "error",
  2634  							Message: "failure",
  2635  							Data:    map[string]interface{}{},
  2636  							Since:   &now,
  2637  						},
  2638  					}}}
  2639  		},
  2640  		func(c *gc.C, st *State) changeTestCase {
  2641  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2642  			u, err := wordpress.AddUnit(AddUnitParams{})
  2643  			c.Assert(err, jc.ErrorIsNil)
  2644  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2645  			c.Assert(err, jc.ErrorIsNil)
  2646  			err = u.AssignToMachine(m)
  2647  			c.Assert(err, jc.ErrorIsNil)
  2648  			MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("17070/udp"))
  2649  			err = u.SetAgentVersion(version.MustParseBinary("2.4.1-ubuntu-amd64"))
  2650  			c.Assert(err, jc.ErrorIsNil)
  2651  			now := st.clock().Now()
  2652  
  2653  			return changeTestCase{
  2654  				about: "unit is updated if it's in backing and in multiwatcher.Store",
  2655  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  2656  					ModelUUID: st.ModelUUID(),
  2657  					Name:      "wordpress/0",
  2658  					AgentStatus: multiwatcher.StatusInfo{
  2659  						Current: "idle",
  2660  						Message: "",
  2661  						Data:    map[string]interface{}{},
  2662  						Since:   &now,
  2663  					},
  2664  					WorkloadStatus: multiwatcher.StatusInfo{
  2665  						Current: "error",
  2666  						Message: "another failure",
  2667  						Data:    map[string]interface{}{},
  2668  						Since:   &now,
  2669  					},
  2670  					OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2671  						allEndpoints: {
  2672  							corenetwork.MustParsePortRange("17070/udp"),
  2673  						},
  2674  					},
  2675  				}},
  2676  				change: watcher.Change{
  2677  					C:  "units",
  2678  					Id: st.docID("wordpress/0"),
  2679  				},
  2680  				expectContents: []multiwatcher.EntityInfo{
  2681  					&multiwatcher.UnitInfo{
  2682  						ModelUUID:   st.ModelUUID(),
  2683  						Name:        "wordpress/0",
  2684  						Application: "wordpress",
  2685  						Base:        "ubuntu@12.10",
  2686  						Life:        life.Alive,
  2687  						MachineID:   "0",
  2688  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2689  							allEndpoints: {
  2690  								corenetwork.MustParsePortRange("17070/udp"),
  2691  							},
  2692  						},
  2693  						AgentStatus: multiwatcher.StatusInfo{
  2694  							Current: "idle",
  2695  							Message: "",
  2696  							Data:    map[string]interface{}{},
  2697  							Version: "2.4.1",
  2698  							Since:   &now,
  2699  						},
  2700  						WorkloadStatus: multiwatcher.StatusInfo{
  2701  							Current: "error",
  2702  							Message: "another failure",
  2703  							Data:    map[string]interface{}{},
  2704  							Since:   &now,
  2705  						},
  2706  					}}}
  2707  		},
  2708  		func(c *gc.C, st *State) changeTestCase {
  2709  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2710  			u, err := wordpress.AddUnit(AddUnitParams{})
  2711  			c.Assert(err, jc.ErrorIsNil)
  2712  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2713  			c.Assert(err, jc.ErrorIsNil)
  2714  			err = u.AssignToMachine(m)
  2715  			c.Assert(err, jc.ErrorIsNil)
  2716  			MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("4242/tcp"))
  2717  
  2718  			return changeTestCase{
  2719  				about: "unit info is updated if a port is opened on the machine it is placed in",
  2720  				initialContents: []multiwatcher.EntityInfo{
  2721  					&multiwatcher.UnitInfo{
  2722  						ModelUUID: st.ModelUUID(),
  2723  						Name:      "wordpress/0",
  2724  					},
  2725  					&multiwatcher.MachineInfo{
  2726  						ModelUUID: st.ModelUUID(),
  2727  						ID:        "0",
  2728  					},
  2729  				},
  2730  				change: watcher.Change{
  2731  					C:  openedPortsC,
  2732  					Id: st.docID("0"),
  2733  				},
  2734  				expectContents: []multiwatcher.EntityInfo{
  2735  					&multiwatcher.UnitInfo{
  2736  						ModelUUID: st.ModelUUID(),
  2737  						Name:      "wordpress/0",
  2738  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2739  							allEndpoints: {
  2740  								corenetwork.MustParsePortRange("4242/tcp"),
  2741  							},
  2742  						},
  2743  					},
  2744  					&multiwatcher.MachineInfo{
  2745  						ModelUUID: st.ModelUUID(),
  2746  						ID:        "0",
  2747  					},
  2748  				}}
  2749  		},
  2750  		func(c *gc.C, st *State) changeTestCase {
  2751  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2752  			u, err := wordpress.AddUnit(AddUnitParams{})
  2753  			c.Assert(err, jc.ErrorIsNil)
  2754  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2755  			c.Assert(err, jc.ErrorIsNil)
  2756  			err = u.AssignToMachine(m)
  2757  			c.Assert(err, jc.ErrorIsNil)
  2758  			MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("21-22/tcp"))
  2759  			now := st.clock().Now()
  2760  			return changeTestCase{
  2761  				about: "unit is created if a port is opened on the machine it is placed in",
  2762  				initialContents: []multiwatcher.EntityInfo{
  2763  					&multiwatcher.MachineInfo{
  2764  						ModelUUID: st.ModelUUID(),
  2765  						ID:        "0",
  2766  					},
  2767  				},
  2768  				change: watcher.Change{
  2769  					C:  "units",
  2770  					Id: st.docID("wordpress/0"),
  2771  				},
  2772  				expectContents: []multiwatcher.EntityInfo{
  2773  					&multiwatcher.UnitInfo{
  2774  						ModelUUID:   st.ModelUUID(),
  2775  						Name:        "wordpress/0",
  2776  						Application: "wordpress",
  2777  						Base:        "ubuntu@12.10",
  2778  						Life:        life.Alive,
  2779  						MachineID:   "0",
  2780  						WorkloadStatus: multiwatcher.StatusInfo{
  2781  							Current: "waiting",
  2782  							Message: "waiting for machine",
  2783  							Data:    map[string]interface{}{},
  2784  							Since:   &now,
  2785  						},
  2786  						AgentStatus: multiwatcher.StatusInfo{
  2787  							Current: "allocating",
  2788  							Data:    map[string]interface{}{},
  2789  							Since:   &now,
  2790  						},
  2791  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2792  							allEndpoints: {
  2793  								corenetwork.MustParsePortRange("21-22/tcp"),
  2794  							},
  2795  						},
  2796  					},
  2797  					&multiwatcher.MachineInfo{
  2798  						ModelUUID: st.ModelUUID(),
  2799  						ID:        "0",
  2800  					},
  2801  				}}
  2802  		},
  2803  		func(c *gc.C, st *State) changeTestCase {
  2804  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2805  			u, err := wordpress.AddUnit(AddUnitParams{})
  2806  			c.Assert(err, jc.ErrorIsNil)
  2807  			m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  2808  			c.Assert(err, jc.ErrorIsNil)
  2809  			err = u.AssignToMachine(m)
  2810  			c.Assert(err, jc.ErrorIsNil)
  2811  			MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp"))
  2812  			publicAddress := network.NewSpaceAddress("public", corenetwork.WithScope(corenetwork.ScopePublic))
  2813  			privateAddress := network.NewSpaceAddress("private", corenetwork.WithScope(corenetwork.ScopeCloudLocal))
  2814  			now := st.clock().Now()
  2815  			sInfo := status.StatusInfo{
  2816  				Status:  status.Error,
  2817  				Message: "failure",
  2818  				Since:   &now,
  2819  			}
  2820  			err = u.SetAgentStatus(sInfo)
  2821  			c.Assert(err, jc.ErrorIsNil)
  2822  
  2823  			return changeTestCase{
  2824  				about: "unit addresses are read from the assigned machine for recent Juju releases",
  2825  				initialContents: []multiwatcher.EntityInfo{
  2826  					&multiwatcher.MachineInfo{
  2827  						ModelUUID:               st.ModelUUID(),
  2828  						ID:                      "0",
  2829  						PreferredPublicAddress:  publicAddress,
  2830  						PreferredPrivateAddress: privateAddress,
  2831  					},
  2832  				},
  2833  				change: watcher.Change{
  2834  					C:  "units",
  2835  					Id: st.docID("wordpress/0"),
  2836  				},
  2837  				expectContents: []multiwatcher.EntityInfo{
  2838  					&multiwatcher.UnitInfo{
  2839  						ModelUUID:      st.ModelUUID(),
  2840  						Name:           "wordpress/0",
  2841  						Application:    "wordpress",
  2842  						Base:           "ubuntu@12.10",
  2843  						Life:           life.Alive,
  2844  						PublicAddress:  "public",
  2845  						PrivateAddress: "private",
  2846  						MachineID:      "0",
  2847  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  2848  							allEndpoints: {
  2849  								corenetwork.MustParsePortRange("12345/tcp"),
  2850  							},
  2851  						},
  2852  						AgentStatus: multiwatcher.StatusInfo{
  2853  							Current: "idle",
  2854  							Message: "",
  2855  							Data:    map[string]interface{}{},
  2856  							Since:   &now,
  2857  						},
  2858  						WorkloadStatus: multiwatcher.StatusInfo{
  2859  							Current: "error",
  2860  							Message: "failure",
  2861  							Data:    map[string]interface{}{},
  2862  							Since:   &now,
  2863  						},
  2864  					},
  2865  					&multiwatcher.MachineInfo{
  2866  						ModelUUID:               st.ModelUUID(),
  2867  						ID:                      "0",
  2868  						PreferredPublicAddress:  publicAddress,
  2869  						PreferredPrivateAddress: privateAddress,
  2870  					},
  2871  				},
  2872  			}
  2873  		},
  2874  		func(c *gc.C, st *State) changeTestCase {
  2875  			return changeTestCase{
  2876  				about: "no unit in state -> do nothing",
  2877  				change: watcher.Change{
  2878  					C:  "statuses",
  2879  					Id: st.docID("u#wordpress/0"),
  2880  				}}
  2881  		},
  2882  		func(c *gc.C, st *State) changeTestCase {
  2883  			now := st.clock().Now()
  2884  			return changeTestCase{
  2885  				about: "no change if status is not in backing",
  2886  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  2887  					ModelUUID:   st.ModelUUID(),
  2888  					Name:        "wordpress/0",
  2889  					Application: "wordpress",
  2890  					AgentStatus: multiwatcher.StatusInfo{
  2891  						Current: "idle",
  2892  						Message: "",
  2893  						Data:    map[string]interface{}{},
  2894  						Since:   &now,
  2895  					},
  2896  					WorkloadStatus: multiwatcher.StatusInfo{
  2897  						Current: "error",
  2898  						Message: "failure",
  2899  						Data:    map[string]interface{}{},
  2900  						Since:   &now,
  2901  					},
  2902  				}},
  2903  				change: watcher.Change{
  2904  					C:  "statuses",
  2905  					Id: st.docID("u#wordpress/0"),
  2906  				},
  2907  				expectContents: []multiwatcher.EntityInfo{
  2908  					&multiwatcher.UnitInfo{
  2909  						ModelUUID:   st.ModelUUID(),
  2910  						Name:        "wordpress/0",
  2911  						Application: "wordpress",
  2912  						AgentStatus: multiwatcher.StatusInfo{
  2913  							Current: "idle",
  2914  							Message: "",
  2915  							Data:    map[string]interface{}{},
  2916  							Since:   &now,
  2917  						},
  2918  						WorkloadStatus: multiwatcher.StatusInfo{
  2919  							Current: "error",
  2920  							Message: "failure",
  2921  							Data:    map[string]interface{}{},
  2922  							Since:   &now,
  2923  						},
  2924  					}}}
  2925  		},
  2926  		func(c *gc.C, st *State) changeTestCase {
  2927  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2928  			u, err := wordpress.AddUnit(AddUnitParams{})
  2929  			c.Assert(err, jc.ErrorIsNil)
  2930  			err = u.AssignToNewMachine()
  2931  			c.Assert(err, jc.ErrorIsNil)
  2932  			now := st.clock().Now()
  2933  			sInfo := status.StatusInfo{
  2934  				Status:  status.Idle,
  2935  				Message: "",
  2936  				Since:   &now,
  2937  			}
  2938  			err = u.SetAgentStatus(sInfo)
  2939  			c.Assert(err, jc.ErrorIsNil)
  2940  
  2941  			return changeTestCase{
  2942  				about: "status is changed if the unit exists in the store",
  2943  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  2944  					ModelUUID:   st.ModelUUID(),
  2945  					Name:        "wordpress/0",
  2946  					Application: "wordpress",
  2947  					AgentStatus: multiwatcher.StatusInfo{
  2948  						Current: "idle",
  2949  						Message: "",
  2950  						Data:    map[string]interface{}{},
  2951  						Since:   &now,
  2952  					},
  2953  					WorkloadStatus: multiwatcher.StatusInfo{
  2954  						Current: "maintenance",
  2955  						Message: "working",
  2956  						Data:    map[string]interface{}{},
  2957  						Since:   &now,
  2958  					},
  2959  				}},
  2960  				change: watcher.Change{
  2961  					C:  "statuses",
  2962  					Id: st.docID("u#wordpress/0"),
  2963  				},
  2964  				expectContents: []multiwatcher.EntityInfo{
  2965  					&multiwatcher.UnitInfo{
  2966  						ModelUUID:   st.ModelUUID(),
  2967  						Name:        "wordpress/0",
  2968  						Application: "wordpress",
  2969  						WorkloadStatus: multiwatcher.StatusInfo{
  2970  							Current: "maintenance",
  2971  							Message: "working",
  2972  							Data:    map[string]interface{}{},
  2973  							Since:   &now,
  2974  						},
  2975  						AgentStatus: multiwatcher.StatusInfo{
  2976  							Current: "idle",
  2977  							Message: "",
  2978  							Data:    map[string]interface{}{},
  2979  							Since:   &now,
  2980  						},
  2981  					}}}
  2982  		},
  2983  		func(c *gc.C, st *State) changeTestCase {
  2984  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  2985  			u, err := wordpress.AddUnit(AddUnitParams{})
  2986  			c.Assert(err, jc.ErrorIsNil)
  2987  			now := st.clock().Now()
  2988  			sInfo := status.StatusInfo{
  2989  				Status:  status.Idle,
  2990  				Message: "",
  2991  				Since:   &now,
  2992  			}
  2993  			err = u.AssignToNewMachine()
  2994  			c.Assert(err, jc.ErrorIsNil)
  2995  			err = u.SetAgentStatus(sInfo)
  2996  			c.Assert(err, jc.ErrorIsNil)
  2997  			sInfo = status.StatusInfo{
  2998  				Status:  status.Maintenance,
  2999  				Message: "doing work",
  3000  				Since:   &now,
  3001  			}
  3002  			err = u.SetStatus(sInfo)
  3003  			c.Assert(err, jc.ErrorIsNil)
  3004  
  3005  			return changeTestCase{
  3006  				about: "unit status is changed if the agent comes off error state",
  3007  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  3008  					ModelUUID:   st.ModelUUID(),
  3009  					Name:        "wordpress/0",
  3010  					Application: "wordpress",
  3011  					AgentStatus: multiwatcher.StatusInfo{
  3012  						Current: "idle",
  3013  						Message: "",
  3014  						Data:    map[string]interface{}{},
  3015  						Since:   &now,
  3016  					},
  3017  					WorkloadStatus: multiwatcher.StatusInfo{
  3018  						Current: "error",
  3019  						Message: "failure",
  3020  						Data:    map[string]interface{}{},
  3021  						Since:   &now,
  3022  					},
  3023  				}},
  3024  				change: watcher.Change{
  3025  					C:  "statuses",
  3026  					Id: st.docID("u#wordpress/0"),
  3027  				},
  3028  				expectContents: []multiwatcher.EntityInfo{
  3029  					&multiwatcher.UnitInfo{
  3030  						ModelUUID:   st.ModelUUID(),
  3031  						Name:        "wordpress/0",
  3032  						Application: "wordpress",
  3033  						WorkloadStatus: multiwatcher.StatusInfo{
  3034  							Current: "maintenance",
  3035  							Message: "doing work",
  3036  							Data:    map[string]interface{}{},
  3037  							Since:   &now,
  3038  						},
  3039  						AgentStatus: multiwatcher.StatusInfo{
  3040  							Current: "idle",
  3041  							Message: "",
  3042  							Data:    map[string]interface{}{},
  3043  							Since:   &now,
  3044  						},
  3045  					}}}
  3046  		},
  3047  		func(c *gc.C, st *State) changeTestCase {
  3048  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  3049  			u, err := wordpress.AddUnit(AddUnitParams{})
  3050  			c.Assert(err, jc.ErrorIsNil)
  3051  			now := st.clock().Now()
  3052  			sInfo := status.StatusInfo{
  3053  				Status:  status.Error,
  3054  				Message: "hook error",
  3055  				Data: map[string]interface{}{
  3056  					"1st-key": "one",
  3057  					"2nd-key": 2,
  3058  					"3rd-key": true,
  3059  				},
  3060  				Since: &now,
  3061  			}
  3062  			err = u.SetAgentStatus(sInfo)
  3063  			c.Assert(err, jc.ErrorIsNil)
  3064  
  3065  			return changeTestCase{
  3066  				about: "agent status is changed with additional status data",
  3067  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  3068  					ModelUUID:   st.ModelUUID(),
  3069  					Name:        "wordpress/0",
  3070  					Application: "wordpress",
  3071  					AgentStatus: multiwatcher.StatusInfo{
  3072  						Current: "idle",
  3073  						Message: "",
  3074  						Data:    map[string]interface{}{},
  3075  						Since:   &now,
  3076  					},
  3077  					WorkloadStatus: multiwatcher.StatusInfo{
  3078  						Current: "active",
  3079  						Since:   &now,
  3080  					},
  3081  				}},
  3082  				change: watcher.Change{
  3083  					C:  "statuses",
  3084  					Id: st.docID("u#wordpress/0"),
  3085  				},
  3086  				expectContents: []multiwatcher.EntityInfo{
  3087  					&multiwatcher.UnitInfo{
  3088  						ModelUUID:   st.ModelUUID(),
  3089  						Name:        "wordpress/0",
  3090  						Application: "wordpress",
  3091  						WorkloadStatus: multiwatcher.StatusInfo{
  3092  							Current: "error",
  3093  							Message: "hook error",
  3094  							Data: map[string]interface{}{
  3095  								"1st-key": "one",
  3096  								"2nd-key": 2,
  3097  								"3rd-key": true,
  3098  							},
  3099  							Since: &now,
  3100  						},
  3101  						AgentStatus: multiwatcher.StatusInfo{
  3102  							Current: "idle",
  3103  							Message: "",
  3104  							Data:    map[string]interface{}{},
  3105  							Since:   &now,
  3106  						},
  3107  					}}}
  3108  		},
  3109  		func(c *gc.C, st *State) changeTestCase {
  3110  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  3111  			u, err := wordpress.AddUnit(AddUnitParams{})
  3112  			c.Assert(err, jc.ErrorIsNil)
  3113  			now := st.clock().Now()
  3114  			sInfo := status.StatusInfo{
  3115  				Status:  status.Error,
  3116  				Message: "hook error",
  3117  				Data: map[string]interface{}{
  3118  					"1st-key": "one",
  3119  					"2nd-key": 2,
  3120  					"3rd-key": true,
  3121  				},
  3122  				Since: &now,
  3123  			}
  3124  			err = u.SetAgentStatus(sInfo)
  3125  			c.Assert(err, jc.ErrorIsNil)
  3126  
  3127  			return changeTestCase{
  3128  				about: "workload status takes into account agent error",
  3129  				initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{
  3130  					ModelUUID:   st.ModelUUID(),
  3131  					Name:        "wordpress/0",
  3132  					Application: "wordpress",
  3133  					WorkloadStatus: multiwatcher.StatusInfo{
  3134  						Current: "error",
  3135  						Message: "hook error",
  3136  						Data: map[string]interface{}{
  3137  							"1st-key": "one",
  3138  							"2nd-key": 2,
  3139  							"3rd-key": true,
  3140  						},
  3141  						Since: &now,
  3142  					},
  3143  					AgentStatus: multiwatcher.StatusInfo{
  3144  						Current: "idle",
  3145  						Message: "",
  3146  						Data:    map[string]interface{}{},
  3147  						Since:   &now,
  3148  					},
  3149  				}},
  3150  				change: watcher.Change{
  3151  					C:  "statuses",
  3152  					Id: st.docID("u#wordpress/0#charm"),
  3153  				},
  3154  				expectContents: []multiwatcher.EntityInfo{
  3155  					&multiwatcher.UnitInfo{
  3156  						ModelUUID:   st.ModelUUID(),
  3157  						Name:        "wordpress/0",
  3158  						Application: "wordpress",
  3159  						WorkloadStatus: multiwatcher.StatusInfo{
  3160  							Current: "error",
  3161  							Message: "hook error",
  3162  							Data: map[string]interface{}{
  3163  								"1st-key": "one",
  3164  								"2nd-key": 2,
  3165  								"3rd-key": true,
  3166  							},
  3167  							Since: &now,
  3168  						},
  3169  						AgentStatus: multiwatcher.StatusInfo{
  3170  							Current: "idle",
  3171  							Message: "",
  3172  							Data:    map[string]interface{}{},
  3173  							Since:   &now,
  3174  						},
  3175  					}}}
  3176  		},
  3177  	}
  3178  	runChangeTests(c, changeTestFuncs)
  3179  }
  3180  
  3181  // initFlag helps to control the different test scenarios.
  3182  type initFlag int
  3183  
  3184  const (
  3185  	assignUnit initFlag = 1
  3186  	openPorts  initFlag = 2
  3187  	closePorts initFlag = 4
  3188  )
  3189  
  3190  func testChangeUnitsNonNilPorts(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) {
  3191  	initModel := func(c *gc.C, st *State, flag initFlag) {
  3192  		wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  3193  		u, err := wordpress.AddUnit(AddUnitParams{})
  3194  		c.Assert(err, jc.ErrorIsNil)
  3195  		m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits)
  3196  		c.Assert(err, jc.ErrorIsNil)
  3197  		if flag&assignUnit != 0 {
  3198  			// Assign the unit.
  3199  			err = u.AssignToMachine(m)
  3200  			c.Assert(err, jc.ErrorIsNil)
  3201  		}
  3202  		if flag&openPorts != 0 {
  3203  			// Add a network to the machine and open a port.
  3204  			publicAddress := network.NewSpaceAddress("1.2.3.4", corenetwork.WithScope(corenetwork.ScopePublic))
  3205  			privateAddress := network.NewSpaceAddress("4.3.2.1", corenetwork.WithScope(corenetwork.ScopeCloudLocal))
  3206  			err = m.SetProviderAddresses(publicAddress, privateAddress)
  3207  			c.Assert(err, jc.ErrorIsNil)
  3208  
  3209  			unitPortRanges, err := u.OpenedPortRanges()
  3210  			if flag&assignUnit == 0 {
  3211  				c.Assert(err, jc.Satisfies, errors.IsNotAssigned)
  3212  			} else {
  3213  				c.Assert(err, jc.ErrorIsNil)
  3214  				unitPortRanges.Open(allEndpoints, corenetwork.MustParsePortRange("12345/tcp"))
  3215  				c.Assert(st.ApplyOperation(unitPortRanges.Changes()), jc.ErrorIsNil)
  3216  			}
  3217  		}
  3218  		if flag&closePorts != 0 {
  3219  			// Close the port again (only if been opened before).
  3220  			unitPortRanges, err := u.OpenedPortRanges()
  3221  			c.Assert(err, jc.ErrorIsNil)
  3222  			unitPortRanges.Close(allEndpoints, corenetwork.MustParsePortRange("12345/tcp"))
  3223  			c.Assert(st.ApplyOperation(unitPortRanges.Changes()), jc.ErrorIsNil)
  3224  		}
  3225  	}
  3226  	changeTestFuncs := []changeTestFunc{
  3227  		func(c *gc.C, st *State) changeTestCase {
  3228  			initModel(c, st, assignUnit)
  3229  			now := st.clock().Now()
  3230  			return changeTestCase{
  3231  				about: "don't open ports on unit",
  3232  				change: watcher.Change{
  3233  					C:  "units",
  3234  					Id: st.docID("wordpress/0"),
  3235  				},
  3236  				expectContents: []multiwatcher.EntityInfo{
  3237  					&multiwatcher.UnitInfo{
  3238  						ModelUUID:   st.ModelUUID(),
  3239  						Name:        "wordpress/0",
  3240  						Application: "wordpress",
  3241  						Base:        "ubuntu@12.10",
  3242  						Life:        life.Alive,
  3243  						MachineID:   "0",
  3244  						WorkloadStatus: multiwatcher.StatusInfo{
  3245  							Current: "waiting",
  3246  							Message: "waiting for machine",
  3247  							Data:    map[string]interface{}{},
  3248  							Since:   &now,
  3249  						},
  3250  						AgentStatus: multiwatcher.StatusInfo{
  3251  							Current: "allocating",
  3252  							Message: "",
  3253  							Data:    map[string]interface{}{},
  3254  							Since:   &now,
  3255  						},
  3256  					}}}
  3257  		},
  3258  		func(c *gc.C, st *State) changeTestCase {
  3259  			initModel(c, st, assignUnit|openPorts)
  3260  			now := st.clock().Now()
  3261  
  3262  			return changeTestCase{
  3263  				about: "open a port on unit",
  3264  				change: watcher.Change{
  3265  					C:  "units",
  3266  					Id: st.docID("wordpress/0"),
  3267  				},
  3268  				expectContents: []multiwatcher.EntityInfo{
  3269  					&multiwatcher.UnitInfo{
  3270  						ModelUUID:   st.ModelUUID(),
  3271  						Name:        "wordpress/0",
  3272  						Application: "wordpress",
  3273  						Base:        "ubuntu@12.10",
  3274  						Life:        life.Alive,
  3275  						MachineID:   "0",
  3276  						OpenPortRangesByEndpoint: network.GroupedPortRanges{
  3277  							allEndpoints: {corenetwork.MustParsePortRange("12345/tcp")},
  3278  						},
  3279  						WorkloadStatus: multiwatcher.StatusInfo{
  3280  							Current: "waiting",
  3281  							Message: "waiting for machine",
  3282  							Data:    map[string]interface{}{},
  3283  							Since:   &now,
  3284  						},
  3285  						AgentStatus: multiwatcher.StatusInfo{
  3286  							Current: "allocating",
  3287  							Message: "",
  3288  							Data:    map[string]interface{}{},
  3289  							Since:   &now,
  3290  						},
  3291  					}}}
  3292  		},
  3293  		func(c *gc.C, st *State) changeTestCase {
  3294  			initModel(c, st, assignUnit|openPorts|closePorts)
  3295  			now := st.clock().Now()
  3296  
  3297  			return changeTestCase{
  3298  				about: "open a port on unit and close it again",
  3299  				change: watcher.Change{
  3300  					C:  "units",
  3301  					Id: st.docID("wordpress/0"),
  3302  				},
  3303  				expectContents: []multiwatcher.EntityInfo{
  3304  					&multiwatcher.UnitInfo{
  3305  						ModelUUID:   st.ModelUUID(),
  3306  						Name:        "wordpress/0",
  3307  						Application: "wordpress",
  3308  						Base:        "ubuntu@12.10",
  3309  						Life:        life.Alive,
  3310  						MachineID:   "0",
  3311  						WorkloadStatus: multiwatcher.StatusInfo{
  3312  							Current: "waiting",
  3313  							Message: "waiting for machine",
  3314  							Data:    map[string]interface{}{},
  3315  							Since:   &now,
  3316  						},
  3317  						AgentStatus: multiwatcher.StatusInfo{
  3318  							Current: "allocating",
  3319  							Message: "",
  3320  							Data:    map[string]interface{}{},
  3321  							Since:   &now,
  3322  						},
  3323  					}}}
  3324  		},
  3325  		func(c *gc.C, st *State) changeTestCase {
  3326  			initModel(c, st, openPorts)
  3327  			now := st.clock().Now()
  3328  
  3329  			return changeTestCase{
  3330  				about: "open ports on an unassigned unit",
  3331  				change: watcher.Change{
  3332  					C:  "units",
  3333  					Id: st.docID("wordpress/0"),
  3334  				},
  3335  				expectContents: []multiwatcher.EntityInfo{
  3336  					&multiwatcher.UnitInfo{
  3337  						ModelUUID:   st.ModelUUID(),
  3338  						Name:        "wordpress/0",
  3339  						Application: "wordpress",
  3340  						Base:        "ubuntu@12.10",
  3341  						Life:        life.Alive,
  3342  						WorkloadStatus: multiwatcher.StatusInfo{
  3343  							Current: "waiting",
  3344  							Message: "waiting for machine",
  3345  							Data:    map[string]interface{}{},
  3346  							Since:   &now,
  3347  						},
  3348  						AgentStatus: multiwatcher.StatusInfo{
  3349  							Current: "allocating",
  3350  							Message: "",
  3351  							Data:    map[string]interface{}{},
  3352  							Since:   &now,
  3353  						},
  3354  					}}}
  3355  		},
  3356  	}
  3357  	runChangeTests(c, changeTestFuncs)
  3358  }
  3359  
  3360  func testChangeRemoteApplications(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  3361  	changeTestFuncs := []changeTestFunc{
  3362  		func(c *gc.C, st *State) changeTestCase {
  3363  			return changeTestCase{
  3364  				about: "no remote application in state, no remote application in store -> do nothing",
  3365  				change: watcher.Change{
  3366  					C:  "remoteApplications",
  3367  					Id: st.docID("remote-mysql2"),
  3368  				}}
  3369  		},
  3370  		func(c *gc.C, st *State) changeTestCase {
  3371  			return changeTestCase{
  3372  				about: "remote application is removed if it's not in backing",
  3373  				initialContents: []multiwatcher.EntityInfo{
  3374  					&multiwatcher.RemoteApplicationUpdate{
  3375  						ModelUUID: st.ModelUUID(),
  3376  						Name:      "remote-mysql2",
  3377  						OfferURL:  "me/model.mysql",
  3378  					},
  3379  				},
  3380  				change: watcher.Change{
  3381  					C:  "remoteApplications",
  3382  					Id: st.docID("remote-mysql2"),
  3383  				}}
  3384  		},
  3385  		func(c *gc.C, st *State) changeTestCase {
  3386  			_, remoteApplicationInfo := addTestingRemoteApplication(
  3387  				c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false)
  3388  			return changeTestCase{
  3389  				about: "remote application is added if it's in backing but not in Store",
  3390  				change: watcher.Change{
  3391  					C:  "remoteApplications",
  3392  					Id: st.docID("remote-mysql2"),
  3393  				},
  3394  				expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo},
  3395  			}
  3396  		},
  3397  		func(c *gc.C, st *State) changeTestCase {
  3398  			// Currently the only change we can make to a remote
  3399  			// application is to destroy it.
  3400  			//
  3401  			// We must add a relation to the remote application, and
  3402  			// a unit to the relation, so that the relation is not
  3403  			// removed and thus the remote application is not removed
  3404  			// upon destroying.
  3405  			wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  3406  			mysql, remoteApplicationInfo := addTestingRemoteApplication(
  3407  				c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false,
  3408  			)
  3409  
  3410  			eps, err := st.InferEndpoints("wordpress", "remote-mysql2")
  3411  			c.Assert(err, jc.ErrorIsNil)
  3412  			rel, err := st.AddRelation(eps[0], eps[1])
  3413  			c.Assert(err, jc.ErrorIsNil)
  3414  			c.Assert(wordpress.Refresh(), jc.ErrorIsNil)
  3415  			c.Assert(mysql.Refresh(), jc.ErrorIsNil)
  3416  
  3417  			wu, err := wordpress.AddUnit(AddUnitParams{})
  3418  			c.Assert(err, jc.ErrorIsNil)
  3419  			wru, err := rel.Unit(wu)
  3420  			c.Assert(err, jc.ErrorIsNil)
  3421  			err = wru.EnterScope(nil)
  3422  			c.Assert(err, jc.ErrorIsNil)
  3423  
  3424  			status, err := mysql.Status()
  3425  			c.Assert(err, jc.ErrorIsNil)
  3426  
  3427  			err = mysql.Destroy()
  3428  			c.Assert(err, jc.ErrorIsNil)
  3429  
  3430  			now := st.clock().Now()
  3431  			initialRemoteApplicationInfo := remoteApplicationInfo
  3432  			remoteApplicationInfo.Life = "dying"
  3433  			remoteApplicationInfo.Status = multiwatcher.StatusInfo{
  3434  				Current: status.Status,
  3435  				Message: status.Message,
  3436  				Data:    status.Data,
  3437  				Since:   &now,
  3438  			}
  3439  			return changeTestCase{
  3440  				about:           "remote application is updated if it's in backing and in multiwatcher.Store",
  3441  				initialContents: []multiwatcher.EntityInfo{&initialRemoteApplicationInfo},
  3442  				change: watcher.Change{
  3443  					C:  "remoteApplications",
  3444  					Id: st.docID("remote-mysql2"),
  3445  				},
  3446  				expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo},
  3447  			}
  3448  		},
  3449  		func(c *gc.C, st *State) changeTestCase {
  3450  			mysql, remoteApplicationInfo := addTestingRemoteApplication(
  3451  				c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false,
  3452  			)
  3453  			now := st.clock().Now()
  3454  			sInfo := status.StatusInfo{
  3455  				Status:  status.Active,
  3456  				Message: "running",
  3457  				Data:    map[string]interface{}{"foo": "bar"},
  3458  				Since:   &now,
  3459  			}
  3460  			err := mysql.SetStatus(sInfo)
  3461  			c.Assert(err, jc.ErrorIsNil)
  3462  			initialRemoteApplicationInfo := remoteApplicationInfo
  3463  			remoteApplicationInfo.Status = multiwatcher.StatusInfo{
  3464  				Current: "active",
  3465  				Message: "running",
  3466  				Data:    map[string]interface{}{"foo": "bar"},
  3467  				Since:   &now,
  3468  			}
  3469  			return changeTestCase{
  3470  				about:           "remote application status is updated if it's in backing and in multiwatcher.Store",
  3471  				initialContents: []multiwatcher.EntityInfo{&initialRemoteApplicationInfo},
  3472  				change: watcher.Change{
  3473  					C:  "statuses",
  3474  					Id: st.docID(mysql.globalKey()),
  3475  				},
  3476  				expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo},
  3477  			}
  3478  		},
  3479  	}
  3480  	runChangeTests(c, changeTestFuncs)
  3481  }
  3482  
  3483  func testChangeApplicationOffers(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  3484  	addOffer := func(c *gc.C, st *State) (multiwatcher.ApplicationOfferInfo, *User) {
  3485  		owner, err := st.AddUser("owner", "owner", "password", "admin")
  3486  		c.Assert(err, jc.ErrorIsNil)
  3487  		AddTestingApplication(c, st, "mysql", AddTestingCharm(c, st, "mysql"))
  3488  		addTestingRemoteApplication(
  3489  			c, st, "remote-wordpress", "", []charm.Relation{{
  3490  				Name:      "db",
  3491  				Role:      "requirer",
  3492  				Scope:     charm.ScopeGlobal,
  3493  				Interface: "mysql",
  3494  			}}, true,
  3495  		)
  3496  		applicationOfferInfo, _ := addTestingApplicationOffer(
  3497  			c, st, owner.UserTag(), "hosted-mysql", "mysql",
  3498  			"quantal-mysql", []string{"server"})
  3499  		return applicationOfferInfo, owner
  3500  	}
  3501  
  3502  	changeTestFuncs := []changeTestFunc{
  3503  		func(c *gc.C, st *State) changeTestCase {
  3504  			return changeTestCase{
  3505  				about: "no application offer in state, no application offer in store -> do nothing",
  3506  				change: watcher.Change{
  3507  					C:  "applicationOffers",
  3508  					Id: st.docID("hosted-mysql"),
  3509  				}}
  3510  		},
  3511  		func(c *gc.C, st *State) changeTestCase {
  3512  			return changeTestCase{
  3513  				about: "application offer is removed if it's not in backing",
  3514  				initialContents: []multiwatcher.EntityInfo{
  3515  					&multiwatcher.ApplicationOfferInfo{
  3516  						ModelUUID:       st.ModelUUID(),
  3517  						OfferName:       "hosted-mysql",
  3518  						OfferUUID:       "hosted-mysql-uuid",
  3519  						ApplicationName: "mysql",
  3520  					},
  3521  				},
  3522  				change: watcher.Change{
  3523  					C:  "applicationOffers",
  3524  					Id: st.docID("hosted-mysql"),
  3525  				}}
  3526  		},
  3527  		func(c *gc.C, st *State) changeTestCase {
  3528  			applicationOfferInfo, _ := addOffer(c, st)
  3529  			return changeTestCase{
  3530  				about: "application offer is added if it's in backing but not in Store",
  3531  				change: watcher.Change{
  3532  					C:  "applicationOffers",
  3533  					Id: st.docID("hosted-mysql"),
  3534  				},
  3535  				expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo},
  3536  			}
  3537  		},
  3538  		func(c *gc.C, st *State) changeTestCase {
  3539  			applicationOfferInfo, owner := addOffer(c, st)
  3540  			app, err := st.Application("mysql")
  3541  			c.Assert(err, jc.ErrorIsNil)
  3542  			curl, _ := app.CharmURL()
  3543  			ch, err := st.Charm(*curl)
  3544  			c.Assert(err, jc.ErrorIsNil)
  3545  			AddTestingApplication(c, st, "another-mysql", ch)
  3546  			offers := NewApplicationOffers(st)
  3547  			_, err = offers.UpdateOffer(crossmodel.AddApplicationOfferArgs{
  3548  				OfferName:       "hosted-mysql",
  3549  				Owner:           owner.Name(),
  3550  				ApplicationName: "another-mysql",
  3551  				Endpoints:       map[string]string{"server": "server"},
  3552  			})
  3553  			c.Assert(err, jc.ErrorIsNil)
  3554  
  3555  			initialApplicationOfferInfo := applicationOfferInfo
  3556  			applicationOfferInfo.ApplicationName = "another-mysql"
  3557  			return changeTestCase{
  3558  				about:           "application offer is updated if it's in backing and not in multiwatcher.Store",
  3559  				initialContents: []multiwatcher.EntityInfo{&initialApplicationOfferInfo},
  3560  				change: watcher.Change{
  3561  					C:  "applicationOffers",
  3562  					Id: st.docID("hosted-mysql"),
  3563  				},
  3564  				expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo},
  3565  			}
  3566  		},
  3567  		func(c *gc.C, st *State) changeTestCase {
  3568  			applicationOfferInfo, _ := addOffer(c, st)
  3569  			initialApplicationOfferInfo := applicationOfferInfo
  3570  			addTestingRemoteApplication(
  3571  				c, st, "remote-wordpress2", "", []charm.Relation{{
  3572  					Name:      "db",
  3573  					Role:      "requirer",
  3574  					Scope:     charm.ScopeGlobal,
  3575  					Interface: "mysql",
  3576  				}}, true,
  3577  			)
  3578  			eps, err := st.InferEndpoints("mysql", "remote-wordpress2")
  3579  			c.Assert(err, jc.ErrorIsNil)
  3580  			rel, err := st.AddRelation(eps...)
  3581  			c.Assert(err, jc.ErrorIsNil)
  3582  			_, err = st.AddOfferConnection(AddOfferConnectionParams{
  3583  				SourceModelUUID: utils.MustNewUUID().String(),
  3584  				RelationId:      rel.Id(),
  3585  				RelationKey:     rel.Tag().Id(),
  3586  				Username:        "fred",
  3587  				OfferUUID:       initialApplicationOfferInfo.OfferUUID,
  3588  			})
  3589  			c.Assert(err, jc.ErrorIsNil)
  3590  
  3591  			applicationOfferInfo.TotalConnectedCount = 2
  3592  			return changeTestCase{
  3593  				about:           "application offer count is updated if it's in backing and in multiwatcher.Store",
  3594  				initialContents: []multiwatcher.EntityInfo{&initialApplicationOfferInfo},
  3595  				change: watcher.Change{
  3596  					C:  "remoteApplications",
  3597  					Id: st.docID("remote-wordpress2"),
  3598  				},
  3599  				expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo},
  3600  			}
  3601  		},
  3602  	}
  3603  	runChangeTests(c, changeTestFuncs)
  3604  }
  3605  
  3606  func testChangeGenerations(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) {
  3607  	changeTestFuncs := []changeTestFunc{
  3608  		func(c *gc.C, st *State) changeTestCase {
  3609  			return changeTestCase{
  3610  				about: "no change if generation absent from state and store",
  3611  				change: watcher.Change{
  3612  					C:  "generations",
  3613  					Id: st.docID("does-not-exist"),
  3614  				}}
  3615  		},
  3616  		func(c *gc.C, st *State) changeTestCase {
  3617  			return changeTestCase{
  3618  				about: "generation is removed if not in backing",
  3619  				initialContents: []multiwatcher.EntityInfo{
  3620  					&multiwatcher.BranchInfo{
  3621  						ModelUUID: st.ModelUUID(),
  3622  						ID:        "to-be-removed",
  3623  					},
  3624  				},
  3625  				change: watcher.Change{
  3626  					C:  "generations",
  3627  					Id: st.docID("to-be-removed"),
  3628  				},
  3629  			}
  3630  		},
  3631  		func(c *gc.C, st *State) changeTestCase {
  3632  			c.Assert(st.AddBranch("new-branch", "some-user"), jc.ErrorIsNil)
  3633  			branch, err := st.Branch("new-branch")
  3634  			c.Assert(err, jc.ErrorIsNil)
  3635  
  3636  			return changeTestCase{
  3637  				about: "generation is added if in backing but not in store",
  3638  				change: watcher.Change{
  3639  					C:  "generations",
  3640  					Id: st.docID(branch.doc.DocId),
  3641  				},
  3642  				expectContents: []multiwatcher.EntityInfo{
  3643  					&multiwatcher.BranchInfo{
  3644  						ModelUUID:     st.ModelUUID(),
  3645  						ID:            st.localID(branch.doc.DocId),
  3646  						Name:          "new-branch",
  3647  						AssignedUnits: map[string][]string{},
  3648  						CreatedBy:     "some-user",
  3649  					}},
  3650  			}
  3651  		},
  3652  		func(c *gc.C, st *State) changeTestCase {
  3653  			c.Assert(st.AddBranch("new-branch", "some-user"), jc.ErrorIsNil)
  3654  			branch, err := st.Branch("new-branch")
  3655  			c.Assert(err, jc.ErrorIsNil)
  3656  
  3657  			app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress"))
  3658  			u, err := app.AddUnit(AddUnitParams{})
  3659  			c.Assert(err, jc.ErrorIsNil)
  3660  
  3661  			c.Assert(branch.AssignUnit(u.Name()), jc.ErrorIsNil)
  3662  
  3663  			return changeTestCase{
  3664  				about: "generation is updated if in backing and in store",
  3665  				change: watcher.Change{
  3666  					C:  "generations",
  3667  					Id: st.docID(branch.doc.DocId),
  3668  				},
  3669  				initialContents: []multiwatcher.EntityInfo{
  3670  					&multiwatcher.BranchInfo{
  3671  						ModelUUID:     st.ModelUUID(),
  3672  						ID:            st.localID(branch.doc.DocId),
  3673  						Name:          "new-branch",
  3674  						AssignedUnits: map[string][]string{},
  3675  						CreatedBy:     "some-user",
  3676  					}},
  3677  				expectContents: []multiwatcher.EntityInfo{
  3678  					&multiwatcher.BranchInfo{
  3679  						ModelUUID:     st.ModelUUID(),
  3680  						ID:            st.localID(branch.doc.DocId),
  3681  						Name:          "new-branch",
  3682  						AssignedUnits: map[string][]string{app.Name(): {u.Name()}},
  3683  						CreatedBy:     "some-user",
  3684  					}},
  3685  			}
  3686  		},
  3687  	}
  3688  	runChangeTests(c, changeTestFuncs)
  3689  }
  3690  
  3691  type entityInfoSlice []multiwatcher.EntityInfo
  3692  
  3693  func (s entityInfoSlice) Len() int      { return len(s) }
  3694  func (s entityInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
  3695  func (s entityInfoSlice) Less(i, j int) bool {
  3696  	id0, id1 := s[i].EntityID(), s[j].EntityID()
  3697  	if id0.Kind != id1.Kind {
  3698  		return id0.Kind < id1.Kind
  3699  	}
  3700  	if id0.ModelUUID != id1.ModelUUID {
  3701  		return id0.ModelUUID < id1.ModelUUID
  3702  	}
  3703  	return id0.ID < id1.ID
  3704  }
  3705  
  3706  func makeActionInfo(a Action, st *State) multiwatcher.ActionInfo {
  3707  	results, message := a.Results()
  3708  	return multiwatcher.ActionInfo{
  3709  		ModelUUID:      st.ModelUUID(),
  3710  		ID:             a.Id(),
  3711  		Receiver:       a.Receiver(),
  3712  		Name:           a.Name(),
  3713  		Parameters:     a.Parameters(),
  3714  		Parallel:       a.Parallel(),
  3715  		ExecutionGroup: a.ExecutionGroup(),
  3716  		Status:         string(a.Status()),
  3717  		Message:        message,
  3718  		Results:        results,
  3719  		Enqueued:       a.Enqueued(),
  3720  		Started:        a.Started(),
  3721  		Completed:      a.Completed(),
  3722  	}
  3723  }
  3724  
  3725  func jcDeepEqualsCheck(c *gc.C, got, want interface{}) bool {
  3726  	ok, err := jc.DeepEqual(got, want)
  3727  	if ok {
  3728  		c.Check(err, jc.ErrorIsNil)
  3729  	}
  3730  	return ok
  3731  }
  3732  
  3733  // assertEntitiesEqual is a specialised version of the typical
  3734  // jc.DeepEquals check that provides more informative output when
  3735  // comparing EntityInfo slices.
  3736  func assertEntitiesEqual(c *gc.C, got, want []multiwatcher.EntityInfo) {
  3737  	if jcDeepEqualsCheck(c, got, want) {
  3738  		return
  3739  	}
  3740  	if len(got) != len(want) {
  3741  		c.Errorf("entity length mismatch; got %d; want %d", len(got), len(want))
  3742  	} else {
  3743  		c.Errorf("entity contents mismatch; same length %d", len(got))
  3744  	}
  3745  	// Lets construct a decent output.
  3746  	var errorOutput string
  3747  	errorOutput = "\ngot: \n"
  3748  	for _, e := range got {
  3749  		errorOutput += fmt.Sprintf("  %T %#v\n", e, e)
  3750  	}
  3751  	errorOutput += "expected: \n"
  3752  	for _, e := range want {
  3753  		errorOutput += fmt.Sprintf("  %T %#v\n", e, e)
  3754  	}
  3755  
  3756  	c.Errorf(errorOutput)
  3757  
  3758  	if len(got) == len(want) {
  3759  		for i := 0; i < len(got); i++ {
  3760  			g := got[i]
  3761  			w := want[i]
  3762  			if !jcDeepEqualsCheck(c, g, w) {
  3763  				if ok := c.Check(g, jc.DeepEquals, w); !ok {
  3764  					c.Logf("first difference at position %d\n", i)
  3765  				}
  3766  				break
  3767  			}
  3768  		}
  3769  	}
  3770  	c.FailNow()
  3771  }