github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/status_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/url"
    11  	"strings"
    12  	"time"
    13  
    14  	gc "launchpad.net/gocheck"
    15  	"launchpad.net/goyaml"
    16  
    17  	"launchpad.net/juju-core/charm"
    18  	"launchpad.net/juju-core/cmd"
    19  	"launchpad.net/juju-core/constraints"
    20  	"launchpad.net/juju-core/instance"
    21  	"launchpad.net/juju-core/juju"
    22  	"launchpad.net/juju-core/juju/testing"
    23  	"launchpad.net/juju-core/state"
    24  	"launchpad.net/juju-core/state/api/params"
    25  	"launchpad.net/juju-core/state/presence"
    26  	coretesting "launchpad.net/juju-core/testing"
    27  	"launchpad.net/juju-core/version"
    28  )
    29  
    30  func runStatus(c *gc.C, args ...string) (code int, stdout, stderr []byte) {
    31  	ctx := coretesting.Context(c)
    32  	code = cmd.Main(&StatusCommand{}, ctx, args)
    33  	stdout = ctx.Stdout.(*bytes.Buffer).Bytes()
    34  	stderr = ctx.Stderr.(*bytes.Buffer).Bytes()
    35  	return
    36  }
    37  
    38  type StatusSuite struct {
    39  	testing.JujuConnSuite
    40  }
    41  
    42  var _ = gc.Suite(&StatusSuite{})
    43  
    44  type M map[string]interface{}
    45  
    46  type L []interface{}
    47  
    48  type testCase struct {
    49  	summary string
    50  	steps   []stepper
    51  }
    52  
    53  func test(summary string, steps ...stepper) testCase {
    54  	return testCase{summary, steps}
    55  }
    56  
    57  type stepper interface {
    58  	step(c *gc.C, ctx *context)
    59  }
    60  
    61  type context struct {
    62  	st      *state.State
    63  	conn    *juju.Conn
    64  	charms  map[string]*state.Charm
    65  	pingers map[string]*presence.Pinger
    66  }
    67  
    68  func (s *StatusSuite) newContext() *context {
    69  	st := s.Conn.Environ.(testing.GetStater).GetStateInAPIServer()
    70  	// We make changes in the API server's state so that
    71  	// our changes to presence are immediately noticed
    72  	// in the status.
    73  	return &context{
    74  		st:      st,
    75  		conn:    s.Conn,
    76  		charms:  make(map[string]*state.Charm),
    77  		pingers: make(map[string]*presence.Pinger),
    78  	}
    79  }
    80  
    81  func (s *StatusSuite) resetContext(c *gc.C, ctx *context) {
    82  	for _, up := range ctx.pingers {
    83  		err := up.Kill()
    84  		c.Check(err, gc.IsNil)
    85  	}
    86  	s.JujuConnSuite.Reset(c)
    87  }
    88  
    89  func (ctx *context) run(c *gc.C, steps []stepper) {
    90  	for i, s := range steps {
    91  		c.Logf("step %d", i)
    92  		c.Logf("%#v", s)
    93  		s.step(c, ctx)
    94  	}
    95  }
    96  
    97  type aliver interface {
    98  	AgentAlive() (bool, error)
    99  	SetAgentAlive() (*presence.Pinger, error)
   100  	WaitAgentAlive(time.Duration) error
   101  }
   102  
   103  func (ctx *context) setAgentAlive(c *gc.C, a aliver) *presence.Pinger {
   104  	pinger, err := a.SetAgentAlive()
   105  	c.Assert(err, gc.IsNil)
   106  	ctx.st.StartSync()
   107  	err = a.WaitAgentAlive(coretesting.LongWait)
   108  	c.Assert(err, gc.IsNil)
   109  	agentAlive, err := a.AgentAlive()
   110  	c.Assert(err, gc.IsNil)
   111  	c.Assert(agentAlive, gc.Equals, true)
   112  	return pinger
   113  }
   114  
   115  // shortcuts for expected output.
   116  var (
   117  	machine0 = M{
   118  		"agent-state": "started",
   119  		"dns-name":    "dummyenv-0.dns",
   120  		"instance-id": "dummyenv-0",
   121  		"series":      "quantal",
   122  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   123  	}
   124  	machine1 = M{
   125  		"agent-state": "started",
   126  		"dns-name":    "dummyenv-1.dns",
   127  		"instance-id": "dummyenv-1",
   128  		"series":      "quantal",
   129  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   130  	}
   131  	machine2 = M{
   132  		"agent-state": "started",
   133  		"dns-name":    "dummyenv-2.dns",
   134  		"instance-id": "dummyenv-2",
   135  		"series":      "quantal",
   136  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   137  	}
   138  	machine3 = M{
   139  		"agent-state": "started",
   140  		"dns-name":    "dummyenv-3.dns",
   141  		"instance-id": "dummyenv-3",
   142  		"series":      "quantal",
   143  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   144  	}
   145  	machine4 = M{
   146  		"agent-state": "started",
   147  		"dns-name":    "dummyenv-4.dns",
   148  		"instance-id": "dummyenv-4",
   149  		"series":      "quantal",
   150  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   151  	}
   152  	machine1WithContainers = M{
   153  		"agent-state": "started",
   154  		"containers": M{
   155  			"1/lxc/0": M{
   156  				"agent-state": "started",
   157  				"containers": M{
   158  					"1/lxc/0/lxc/0": M{
   159  						"agent-state": "started",
   160  						"dns-name":    "dummyenv-3.dns",
   161  						"instance-id": "dummyenv-3",
   162  						"series":      "quantal",
   163  					},
   164  				},
   165  				"dns-name":    "dummyenv-2.dns",
   166  				"instance-id": "dummyenv-2",
   167  				"series":      "quantal",
   168  			},
   169  			"1/lxc/1": M{
   170  				"instance-id": "pending",
   171  				"series":      "quantal",
   172  			},
   173  		},
   174  		"dns-name":    "dummyenv-1.dns",
   175  		"instance-id": "dummyenv-1",
   176  		"series":      "quantal",
   177  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   178  	}
   179  	machine1WithContainersScoped = M{
   180  		"agent-state": "started",
   181  		"containers": M{
   182  			"1/lxc/0": M{
   183  				"agent-state": "started",
   184  				"dns-name":    "dummyenv-2.dns",
   185  				"instance-id": "dummyenv-2",
   186  				"series":      "quantal",
   187  			},
   188  		},
   189  		"dns-name":    "dummyenv-1.dns",
   190  		"instance-id": "dummyenv-1",
   191  		"series":      "quantal",
   192  		"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   193  	}
   194  	unexposedService = M{
   195  		"charm":   "cs:quantal/dummy-1",
   196  		"exposed": false,
   197  	}
   198  	exposedService = M{
   199  		"charm":   "cs:quantal/dummy-1",
   200  		"exposed": true,
   201  	}
   202  )
   203  
   204  type outputFormat struct {
   205  	name      string
   206  	marshal   func(v interface{}) ([]byte, error)
   207  	unmarshal func(data []byte, v interface{}) error
   208  }
   209  
   210  // statusFormats list all output formats supported by status command.
   211  var statusFormats = []outputFormat{
   212  	{"yaml", goyaml.Marshal, goyaml.Unmarshal},
   213  	{"json", json.Marshal, json.Unmarshal},
   214  }
   215  
   216  var machineCons = constraints.MustParse("cpu-cores=2 mem=8G root-disk=8G")
   217  
   218  var statusTests = []testCase{
   219  	// Status tests
   220  	test(
   221  		"bootstrap and starting a single instance",
   222  
   223  		// unlikely, as you can't run juju status in real life without
   224  		// machine/0 bootstrapped.
   225  		expect{
   226  			"empty state",
   227  			M{
   228  				"environment": "dummyenv",
   229  				"machines":    M{},
   230  				"services":    M{},
   231  			},
   232  		},
   233  
   234  		addMachine{machineId: "0", job: state.JobManageEnviron},
   235  		expect{
   236  			"simulate juju bootstrap by adding machine/0 to the state",
   237  			M{
   238  				"environment": "dummyenv",
   239  				"machines": M{
   240  					"0": M{
   241  						"instance-id": "pending",
   242  						"series":      "quantal",
   243  					},
   244  				},
   245  				"services": M{},
   246  			},
   247  		},
   248  
   249  		startAliveMachine{"0"},
   250  		setAddresses{"0", []instance.Address{
   251  			instance.NewAddress("10.0.0.1"),
   252  			instance.NewAddress("dummyenv-0.dns"),
   253  		}},
   254  		expect{
   255  			"simulate the PA starting an instance in response to the state change",
   256  			M{
   257  				"environment": "dummyenv",
   258  				"machines": M{
   259  					"0": M{
   260  						"agent-state": "pending",
   261  						"dns-name":    "dummyenv-0.dns",
   262  						"instance-id": "dummyenv-0",
   263  						"series":      "quantal",
   264  						"hardware":    "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   265  					},
   266  				},
   267  				"services": M{},
   268  			},
   269  		},
   270  
   271  		setMachineStatus{"0", params.StatusStarted, ""},
   272  		expect{
   273  			"simulate the MA started and set the machine status",
   274  			M{
   275  				"environment": "dummyenv",
   276  				"machines": M{
   277  					"0": machine0,
   278  				},
   279  				"services": M{},
   280  			},
   281  		},
   282  
   283  		setTools{"0", version.MustParseBinary("1.2.3-gutsy-ppc")},
   284  		expect{
   285  			"simulate the MA setting the version",
   286  			M{
   287  				"environment": "dummyenv",
   288  				"machines": M{
   289  					"0": M{
   290  						"dns-name":      "dummyenv-0.dns",
   291  						"instance-id":   "dummyenv-0",
   292  						"agent-version": "1.2.3",
   293  						"agent-state":   "started",
   294  						"series":        "quantal",
   295  						"hardware":      "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   296  					},
   297  				},
   298  				"services": M{},
   299  			},
   300  		},
   301  	), test(
   302  		"instance with different hardware characteristics",
   303  		addMachine{"0", machineCons, state.JobManageEnviron},
   304  		setAddresses{"0", []instance.Address{
   305  			instance.NewAddress("10.0.0.1"),
   306  			instance.NewAddress("dummyenv-0.dns"),
   307  		}},
   308  		startAliveMachine{"0"},
   309  		setMachineStatus{"0", params.StatusStarted, ""},
   310  		expect{
   311  			"machine 0 has specific hardware characteristics",
   312  			M{
   313  				"environment": "dummyenv",
   314  				"machines": M{
   315  					"0": M{
   316  						"agent-state": "started",
   317  						"dns-name":    "dummyenv-0.dns",
   318  						"instance-id": "dummyenv-0",
   319  						"series":      "quantal",
   320  						"hardware":    "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M",
   321  					},
   322  				},
   323  				"services": M{},
   324  			},
   325  		},
   326  	), test(
   327  		"instance without addresses",
   328  		addMachine{"0", machineCons, state.JobManageEnviron},
   329  		startAliveMachine{"0"},
   330  		setMachineStatus{"0", params.StatusStarted, ""},
   331  		expect{
   332  			"machine 0 has no dns-name",
   333  			M{
   334  				"environment": "dummyenv",
   335  				"machines": M{
   336  					"0": M{
   337  						"agent-state": "started",
   338  						"instance-id": "dummyenv-0",
   339  						"series":      "quantal",
   340  						"hardware":    "arch=amd64 cpu-cores=2 mem=8192M root-disk=8192M",
   341  					},
   342  				},
   343  				"services": M{},
   344  			},
   345  		},
   346  	), test(
   347  		"test pending and missing machines",
   348  		addMachine{machineId: "0", job: state.JobManageEnviron},
   349  		expect{
   350  			"machine 0 reports pending",
   351  			M{
   352  				"environment": "dummyenv",
   353  				"machines": M{
   354  					"0": M{
   355  						"instance-id": "pending",
   356  						"series":      "quantal",
   357  					},
   358  				},
   359  				"services": M{},
   360  			},
   361  		},
   362  
   363  		startMissingMachine{"0"},
   364  		expect{
   365  			"machine 0 reports missing",
   366  			M{
   367  				"environment": "dummyenv",
   368  				"machines": M{
   369  					"0": M{
   370  						"instance-state": "missing",
   371  						"instance-id":    "i-missing",
   372  						"agent-state":    "pending",
   373  						"series":         "quantal",
   374  						"hardware":       "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   375  					},
   376  				},
   377  				"services": M{},
   378  			},
   379  		},
   380  	), test(
   381  		"add two services and expose one, then add 2 more machines and some units",
   382  		addMachine{machineId: "0", job: state.JobManageEnviron},
   383  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
   384  		startAliveMachine{"0"},
   385  		setMachineStatus{"0", params.StatusStarted, ""},
   386  		addCharm{"dummy"},
   387  		addService{"dummy-service", "dummy"},
   388  		addService{"exposed-service", "dummy"},
   389  		expect{
   390  			"no services exposed yet",
   391  			M{
   392  				"environment": "dummyenv",
   393  				"machines": M{
   394  					"0": machine0,
   395  				},
   396  				"services": M{
   397  					"dummy-service":   unexposedService,
   398  					"exposed-service": unexposedService,
   399  				},
   400  			},
   401  		},
   402  
   403  		setServiceExposed{"exposed-service", true},
   404  		expect{
   405  			"one exposed service",
   406  			M{
   407  				"environment": "dummyenv",
   408  				"machines": M{
   409  					"0": machine0,
   410  				},
   411  				"services": M{
   412  					"dummy-service":   unexposedService,
   413  					"exposed-service": exposedService,
   414  				},
   415  			},
   416  		},
   417  
   418  		addMachine{machineId: "1", job: state.JobHostUnits},
   419  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
   420  		startAliveMachine{"1"},
   421  		setMachineStatus{"1", params.StatusStarted, ""},
   422  		addMachine{machineId: "2", job: state.JobHostUnits},
   423  		setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}},
   424  		startAliveMachine{"2"},
   425  		setMachineStatus{"2", params.StatusStarted, ""},
   426  		expect{
   427  			"two more machines added",
   428  			M{
   429  				"environment": "dummyenv",
   430  				"machines": M{
   431  					"0": machine0,
   432  					"1": machine1,
   433  					"2": machine2,
   434  				},
   435  				"services": M{
   436  					"dummy-service":   unexposedService,
   437  					"exposed-service": exposedService,
   438  				},
   439  			},
   440  		},
   441  
   442  		addUnit{"dummy-service", "1"},
   443  		addAliveUnit{"exposed-service", "2"},
   444  		setUnitStatus{"exposed-service/0", params.StatusError, "You Require More Vespene Gas"},
   445  		// Open multiple ports with different protocols,
   446  		// ensure they're sorted on protocol, then number.
   447  		openUnitPort{"exposed-service/0", "udp", 10},
   448  		openUnitPort{"exposed-service/0", "udp", 2},
   449  		openUnitPort{"exposed-service/0", "tcp", 3},
   450  		openUnitPort{"exposed-service/0", "tcp", 2},
   451  		// Simulate some status with no info, while the agent is down.
   452  		setUnitStatus{"dummy-service/0", params.StatusStarted, ""},
   453  		expect{
   454  			"add two units, one alive (in error state), one down",
   455  			M{
   456  				"environment": "dummyenv",
   457  				"machines": M{
   458  					"0": machine0,
   459  					"1": machine1,
   460  					"2": machine2,
   461  				},
   462  				"services": M{
   463  					"exposed-service": M{
   464  						"charm":   "cs:quantal/dummy-1",
   465  						"exposed": true,
   466  						"units": M{
   467  							"exposed-service/0": M{
   468  								"machine":          "2",
   469  								"agent-state":      "error",
   470  								"agent-state-info": "You Require More Vespene Gas",
   471  								"open-ports": L{
   472  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   473  								},
   474  								"public-address": "dummyenv-2.dns",
   475  							},
   476  						},
   477  					},
   478  					"dummy-service": M{
   479  						"charm":   "cs:quantal/dummy-1",
   480  						"exposed": false,
   481  						"units": M{
   482  							"dummy-service/0": M{
   483  								"machine":          "1",
   484  								"agent-state":      "down",
   485  								"agent-state-info": "(started)",
   486  								"public-address":   "dummyenv-1.dns",
   487  							},
   488  						},
   489  					},
   490  				},
   491  			},
   492  		},
   493  
   494  		addMachine{machineId: "3", job: state.JobHostUnits},
   495  		startMachine{"3"},
   496  		// Simulate some status with info, while the agent is down.
   497  		setAddresses{"3", []instance.Address{instance.NewAddress("dummyenv-3.dns")}},
   498  		setMachineStatus{"3", params.StatusStopped, "Really?"},
   499  		addMachine{machineId: "4", job: state.JobHostUnits},
   500  		setAddresses{"4", []instance.Address{instance.NewAddress("dummyenv-4.dns")}},
   501  		startAliveMachine{"4"},
   502  		setMachineStatus{"4", params.StatusError, "Beware the red toys"},
   503  		ensureDyingUnit{"dummy-service/0"},
   504  		addMachine{machineId: "5", job: state.JobHostUnits},
   505  		ensureDeadMachine{"5"},
   506  		expect{
   507  			"add three more machine, one with a dead agent, one in error state and one dead itself; also one dying unit",
   508  			M{
   509  				"environment": "dummyenv",
   510  				"machines": M{
   511  					"0": machine0,
   512  					"1": machine1,
   513  					"2": machine2,
   514  					"3": M{
   515  						"dns-name":         "dummyenv-3.dns",
   516  						"instance-id":      "dummyenv-3",
   517  						"agent-state":      "down",
   518  						"agent-state-info": "(stopped: Really?)",
   519  						"series":           "quantal",
   520  						"hardware":         "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   521  					},
   522  					"4": M{
   523  						"dns-name":         "dummyenv-4.dns",
   524  						"instance-id":      "dummyenv-4",
   525  						"agent-state":      "error",
   526  						"agent-state-info": "Beware the red toys",
   527  						"series":           "quantal",
   528  						"hardware":         "arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M",
   529  					},
   530  					"5": M{
   531  						"life":        "dead",
   532  						"instance-id": "pending",
   533  						"series":      "quantal",
   534  					},
   535  				},
   536  				"services": M{
   537  					"exposed-service": M{
   538  						"charm":   "cs:quantal/dummy-1",
   539  						"exposed": true,
   540  						"units": M{
   541  							"exposed-service/0": M{
   542  								"machine":          "2",
   543  								"agent-state":      "error",
   544  								"agent-state-info": "You Require More Vespene Gas",
   545  								"open-ports": L{
   546  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   547  								},
   548  								"public-address": "dummyenv-2.dns",
   549  							},
   550  						},
   551  					},
   552  					"dummy-service": M{
   553  						"charm":   "cs:quantal/dummy-1",
   554  						"exposed": false,
   555  						"units": M{
   556  							"dummy-service/0": M{
   557  								"machine":          "1",
   558  								"life":             "dying",
   559  								"agent-state":      "down",
   560  								"agent-state-info": "(started)",
   561  								"public-address":   "dummyenv-1.dns",
   562  							},
   563  						},
   564  					},
   565  				},
   566  			},
   567  		},
   568  
   569  		scopedExpect{
   570  			"scope status on dummy-service/0 unit",
   571  			[]string{"dummy-service/0"},
   572  			M{
   573  				"environment": "dummyenv",
   574  				"machines": M{
   575  					"1": machine1,
   576  				},
   577  				"services": M{
   578  					"dummy-service": M{
   579  						"charm":   "cs:quantal/dummy-1",
   580  						"exposed": false,
   581  						"units": M{
   582  							"dummy-service/0": M{
   583  								"machine":          "1",
   584  								"life":             "dying",
   585  								"agent-state":      "down",
   586  								"agent-state-info": "(started)",
   587  								"public-address":   "dummyenv-1.dns",
   588  							},
   589  						},
   590  					},
   591  				},
   592  			},
   593  		},
   594  		scopedExpect{
   595  			"scope status on exposed-service service",
   596  			[]string{"exposed-service"},
   597  			M{
   598  				"environment": "dummyenv",
   599  				"machines": M{
   600  					"2": machine2,
   601  				},
   602  				"services": M{
   603  					"exposed-service": M{
   604  						"charm":   "cs:quantal/dummy-1",
   605  						"exposed": true,
   606  						"units": M{
   607  							"exposed-service/0": M{
   608  								"machine":          "2",
   609  								"agent-state":      "error",
   610  								"agent-state-info": "You Require More Vespene Gas",
   611  								"open-ports": L{
   612  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   613  								},
   614  								"public-address": "dummyenv-2.dns",
   615  							},
   616  						},
   617  					},
   618  				},
   619  			},
   620  		},
   621  		scopedExpect{
   622  			"scope status on service pattern",
   623  			[]string{"d*-service"},
   624  			M{
   625  				"environment": "dummyenv",
   626  				"machines": M{
   627  					"1": machine1,
   628  				},
   629  				"services": M{
   630  					"dummy-service": M{
   631  						"charm":   "cs:quantal/dummy-1",
   632  						"exposed": false,
   633  						"units": M{
   634  							"dummy-service/0": M{
   635  								"machine":          "1",
   636  								"life":             "dying",
   637  								"agent-state":      "down",
   638  								"agent-state-info": "(started)",
   639  								"public-address":   "dummyenv-1.dns",
   640  							},
   641  						},
   642  					},
   643  				},
   644  			},
   645  		},
   646  		scopedExpect{
   647  			"scope status on unit pattern",
   648  			[]string{"e*posed-service/*"},
   649  			M{
   650  				"environment": "dummyenv",
   651  				"machines": M{
   652  					"2": machine2,
   653  				},
   654  				"services": M{
   655  					"exposed-service": M{
   656  						"charm":   "cs:quantal/dummy-1",
   657  						"exposed": true,
   658  						"units": M{
   659  							"exposed-service/0": M{
   660  								"machine":          "2",
   661  								"agent-state":      "error",
   662  								"agent-state-info": "You Require More Vespene Gas",
   663  								"open-ports": L{
   664  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   665  								},
   666  								"public-address": "dummyenv-2.dns",
   667  							},
   668  						},
   669  					},
   670  				},
   671  			},
   672  		},
   673  		scopedExpect{
   674  			"scope status on combination of service and unit patterns",
   675  			[]string{"exposed-service", "dummy-service", "e*posed-service/*", "dummy-service/*"},
   676  			M{
   677  				"environment": "dummyenv",
   678  				"machines": M{
   679  					"1": machine1,
   680  					"2": machine2,
   681  				},
   682  				"services": M{
   683  					"dummy-service": M{
   684  						"charm":   "cs:quantal/dummy-1",
   685  						"exposed": false,
   686  						"units": M{
   687  							"dummy-service/0": M{
   688  								"machine":          "1",
   689  								"life":             "dying",
   690  								"agent-state":      "down",
   691  								"agent-state-info": "(started)",
   692  								"public-address":   "dummyenv-1.dns",
   693  							},
   694  						},
   695  					},
   696  					"exposed-service": M{
   697  						"charm":   "cs:quantal/dummy-1",
   698  						"exposed": true,
   699  						"units": M{
   700  							"exposed-service/0": M{
   701  								"machine":          "2",
   702  								"agent-state":      "error",
   703  								"agent-state-info": "You Require More Vespene Gas",
   704  								"open-ports": L{
   705  									"2/tcp", "3/tcp", "2/udp", "10/udp",
   706  								},
   707  								"public-address": "dummyenv-2.dns",
   708  							},
   709  						},
   710  					},
   711  				},
   712  			},
   713  		},
   714  	),
   715  	test(
   716  		"add a dying service",
   717  		addCharm{"dummy"},
   718  		addService{"dummy-service", "dummy"},
   719  		addMachine{machineId: "0", job: state.JobHostUnits},
   720  		addUnit{"dummy-service", "0"},
   721  		ensureDyingService{"dummy-service"},
   722  		expect{
   723  			"service shows life==dying",
   724  			M{
   725  				"environment": "dummyenv",
   726  				"machines": M{
   727  					"0": M{
   728  						"instance-id": "pending",
   729  						"series":      "quantal",
   730  					},
   731  				},
   732  				"services": M{
   733  					"dummy-service": M{
   734  						"charm":   "cs:quantal/dummy-1",
   735  						"exposed": false,
   736  						"life":    "dying",
   737  						"units": M{
   738  							"dummy-service/0": M{
   739  								"machine":     "0",
   740  								"agent-state": "pending",
   741  							},
   742  						},
   743  					},
   744  				},
   745  			},
   746  		},
   747  	),
   748  
   749  	// Relation tests
   750  	test(
   751  		"complex scenario with multiple related services",
   752  		addMachine{machineId: "0", job: state.JobManageEnviron},
   753  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
   754  		startAliveMachine{"0"},
   755  		setMachineStatus{"0", params.StatusStarted, ""},
   756  		addCharm{"wordpress"},
   757  		addCharm{"mysql"},
   758  		addCharm{"varnish"},
   759  
   760  		addService{"project", "wordpress"},
   761  		setServiceExposed{"project", true},
   762  		addMachine{machineId: "1", job: state.JobHostUnits},
   763  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
   764  		startAliveMachine{"1"},
   765  		setMachineStatus{"1", params.StatusStarted, ""},
   766  		addAliveUnit{"project", "1"},
   767  		setUnitStatus{"project/0", params.StatusStarted, ""},
   768  
   769  		addService{"mysql", "mysql"},
   770  		setServiceExposed{"mysql", true},
   771  		addMachine{machineId: "2", job: state.JobHostUnits},
   772  		setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}},
   773  		startAliveMachine{"2"},
   774  		setMachineStatus{"2", params.StatusStarted, ""},
   775  		addAliveUnit{"mysql", "2"},
   776  		setUnitStatus{"mysql/0", params.StatusStarted, ""},
   777  
   778  		addService{"varnish", "varnish"},
   779  		setServiceExposed{"varnish", true},
   780  		addMachine{machineId: "3", job: state.JobHostUnits},
   781  		setAddresses{"3", []instance.Address{instance.NewAddress("dummyenv-3.dns")}},
   782  		startAliveMachine{"3"},
   783  		setMachineStatus{"3", params.StatusStarted, ""},
   784  		addUnit{"varnish", "3"},
   785  
   786  		addService{"private", "wordpress"},
   787  		setServiceExposed{"private", true},
   788  		addMachine{machineId: "4", job: state.JobHostUnits},
   789  		setAddresses{"4", []instance.Address{instance.NewAddress("dummyenv-4.dns")}},
   790  		startAliveMachine{"4"},
   791  		setMachineStatus{"4", params.StatusStarted, ""},
   792  		addUnit{"private", "4"},
   793  
   794  		relateServices{"project", "mysql"},
   795  		relateServices{"project", "varnish"},
   796  		relateServices{"private", "mysql"},
   797  
   798  		expect{
   799  			"multiples services with relations between some of them",
   800  			M{
   801  				"environment": "dummyenv",
   802  				"machines": M{
   803  					"0": machine0,
   804  					"1": machine1,
   805  					"2": machine2,
   806  					"3": machine3,
   807  					"4": machine4,
   808  				},
   809  				"services": M{
   810  					"project": M{
   811  						"charm":   "cs:quantal/wordpress-3",
   812  						"exposed": true,
   813  						"units": M{
   814  							"project/0": M{
   815  								"machine":        "1",
   816  								"agent-state":    "started",
   817  								"public-address": "dummyenv-1.dns",
   818  							},
   819  						},
   820  						"relations": M{
   821  							"db":    L{"mysql"},
   822  							"cache": L{"varnish"},
   823  						},
   824  					},
   825  					"mysql": M{
   826  						"charm":   "cs:quantal/mysql-1",
   827  						"exposed": true,
   828  						"units": M{
   829  							"mysql/0": M{
   830  								"machine":        "2",
   831  								"agent-state":    "started",
   832  								"public-address": "dummyenv-2.dns",
   833  							},
   834  						},
   835  						"relations": M{
   836  							"server": L{"private", "project"},
   837  						},
   838  					},
   839  					"varnish": M{
   840  						"charm":   "cs:quantal/varnish-1",
   841  						"exposed": true,
   842  						"units": M{
   843  							"varnish/0": M{
   844  								"machine":        "3",
   845  								"agent-state":    "pending",
   846  								"public-address": "dummyenv-3.dns",
   847  							},
   848  						},
   849  						"relations": M{
   850  							"webcache": L{"project"},
   851  						},
   852  					},
   853  					"private": M{
   854  						"charm":   "cs:quantal/wordpress-3",
   855  						"exposed": true,
   856  						"units": M{
   857  							"private/0": M{
   858  								"machine":        "4",
   859  								"agent-state":    "pending",
   860  								"public-address": "dummyenv-4.dns",
   861  							},
   862  						},
   863  						"relations": M{
   864  							"db": L{"mysql"},
   865  						},
   866  					},
   867  				},
   868  			},
   869  		},
   870  	), test(
   871  		"simple peer scenario",
   872  		addMachine{machineId: "0", job: state.JobManageEnviron},
   873  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
   874  		startAliveMachine{"0"},
   875  		setMachineStatus{"0", params.StatusStarted, ""},
   876  		addCharm{"riak"},
   877  		addCharm{"wordpress"},
   878  
   879  		addService{"riak", "riak"},
   880  		setServiceExposed{"riak", true},
   881  		addMachine{machineId: "1", job: state.JobHostUnits},
   882  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
   883  		startAliveMachine{"1"},
   884  		setMachineStatus{"1", params.StatusStarted, ""},
   885  		addAliveUnit{"riak", "1"},
   886  		setUnitStatus{"riak/0", params.StatusStarted, ""},
   887  		addMachine{machineId: "2", job: state.JobHostUnits},
   888  		setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}},
   889  		startAliveMachine{"2"},
   890  		setMachineStatus{"2", params.StatusStarted, ""},
   891  		addAliveUnit{"riak", "2"},
   892  		setUnitStatus{"riak/1", params.StatusStarted, ""},
   893  		addMachine{machineId: "3", job: state.JobHostUnits},
   894  		setAddresses{"3", []instance.Address{instance.NewAddress("dummyenv-3.dns")}},
   895  		startAliveMachine{"3"},
   896  		setMachineStatus{"3", params.StatusStarted, ""},
   897  		addAliveUnit{"riak", "3"},
   898  		setUnitStatus{"riak/2", params.StatusStarted, ""},
   899  
   900  		expect{
   901  			"multiples related peer units",
   902  			M{
   903  				"environment": "dummyenv",
   904  				"machines": M{
   905  					"0": machine0,
   906  					"1": machine1,
   907  					"2": machine2,
   908  					"3": machine3,
   909  				},
   910  				"services": M{
   911  					"riak": M{
   912  						"charm":   "cs:quantal/riak-7",
   913  						"exposed": true,
   914  						"units": M{
   915  							"riak/0": M{
   916  								"machine":        "1",
   917  								"agent-state":    "started",
   918  								"public-address": "dummyenv-1.dns",
   919  							},
   920  							"riak/1": M{
   921  								"machine":        "2",
   922  								"agent-state":    "started",
   923  								"public-address": "dummyenv-2.dns",
   924  							},
   925  							"riak/2": M{
   926  								"machine":        "3",
   927  								"agent-state":    "started",
   928  								"public-address": "dummyenv-3.dns",
   929  							},
   930  						},
   931  						"relations": M{
   932  							"ring": L{"riak"},
   933  						},
   934  					},
   935  				},
   936  			},
   937  		},
   938  	),
   939  
   940  	// Subordinate tests
   941  	test(
   942  		"one service with one subordinate service",
   943  		addMachine{machineId: "0", job: state.JobManageEnviron},
   944  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
   945  		startAliveMachine{"0"},
   946  		setMachineStatus{"0", params.StatusStarted, ""},
   947  		addCharm{"wordpress"},
   948  		addCharm{"mysql"},
   949  		addCharm{"logging"},
   950  
   951  		addService{"wordpress", "wordpress"},
   952  		setServiceExposed{"wordpress", true},
   953  		addMachine{machineId: "1", job: state.JobHostUnits},
   954  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
   955  		startAliveMachine{"1"},
   956  		setMachineStatus{"1", params.StatusStarted, ""},
   957  		addAliveUnit{"wordpress", "1"},
   958  		setUnitStatus{"wordpress/0", params.StatusStarted, ""},
   959  
   960  		addService{"mysql", "mysql"},
   961  		setServiceExposed{"mysql", true},
   962  		addMachine{machineId: "2", job: state.JobHostUnits},
   963  		setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}},
   964  		startAliveMachine{"2"},
   965  		setMachineStatus{"2", params.StatusStarted, ""},
   966  		addAliveUnit{"mysql", "2"},
   967  		setUnitStatus{"mysql/0", params.StatusStarted, ""},
   968  
   969  		addService{"logging", "logging"},
   970  		setServiceExposed{"logging", true},
   971  
   972  		relateServices{"wordpress", "mysql"},
   973  		relateServices{"wordpress", "logging"},
   974  		relateServices{"mysql", "logging"},
   975  
   976  		addSubordinate{"wordpress/0", "logging"},
   977  		addSubordinate{"mysql/0", "logging"},
   978  
   979  		setUnitsAlive{"logging"},
   980  		setUnitStatus{"logging/0", params.StatusStarted, ""},
   981  		setUnitStatus{"logging/1", params.StatusError, "somehow lost in all those logs"},
   982  
   983  		expect{
   984  			"multiples related peer units",
   985  			M{
   986  				"environment": "dummyenv",
   987  				"machines": M{
   988  					"0": machine0,
   989  					"1": machine1,
   990  					"2": machine2,
   991  				},
   992  				"services": M{
   993  					"wordpress": M{
   994  						"charm":   "cs:quantal/wordpress-3",
   995  						"exposed": true,
   996  						"units": M{
   997  							"wordpress/0": M{
   998  								"machine":     "1",
   999  								"agent-state": "started",
  1000  								"subordinates": M{
  1001  									"logging/0": M{
  1002  										"agent-state": "started",
  1003  									},
  1004  								},
  1005  								"public-address": "dummyenv-1.dns",
  1006  							},
  1007  						},
  1008  						"relations": M{
  1009  							"db":          L{"mysql"},
  1010  							"logging-dir": L{"logging"},
  1011  						},
  1012  					},
  1013  					"mysql": M{
  1014  						"charm":   "cs:quantal/mysql-1",
  1015  						"exposed": true,
  1016  						"units": M{
  1017  							"mysql/0": M{
  1018  								"machine":     "2",
  1019  								"agent-state": "started",
  1020  								"subordinates": M{
  1021  									"logging/1": M{
  1022  										"agent-state":      "error",
  1023  										"agent-state-info": "somehow lost in all those logs",
  1024  									},
  1025  								},
  1026  								"public-address": "dummyenv-2.dns",
  1027  							},
  1028  						},
  1029  						"relations": M{
  1030  							"server":    L{"wordpress"},
  1031  							"juju-info": L{"logging"},
  1032  						},
  1033  					},
  1034  					"logging": M{
  1035  						"charm":   "cs:quantal/logging-1",
  1036  						"exposed": true,
  1037  						"relations": M{
  1038  							"logging-directory": L{"wordpress"},
  1039  							"info":              L{"mysql"},
  1040  						},
  1041  						"subordinate-to": L{"mysql", "wordpress"},
  1042  					},
  1043  				},
  1044  			},
  1045  		},
  1046  
  1047  		// scoped on 'logging'
  1048  		scopedExpect{
  1049  			"subordinates scoped on logging",
  1050  			[]string{"logging"},
  1051  			M{
  1052  				"environment": "dummyenv",
  1053  				"machines": M{
  1054  					"1": machine1,
  1055  					"2": machine2,
  1056  				},
  1057  				"services": M{
  1058  					"wordpress": M{
  1059  						"charm":   "cs:quantal/wordpress-3",
  1060  						"exposed": true,
  1061  						"units": M{
  1062  							"wordpress/0": M{
  1063  								"machine":     "1",
  1064  								"agent-state": "started",
  1065  								"subordinates": M{
  1066  									"logging/0": M{
  1067  										"agent-state": "started",
  1068  									},
  1069  								},
  1070  								"public-address": "dummyenv-1.dns",
  1071  							},
  1072  						},
  1073  						"relations": M{
  1074  							"db":          L{"mysql"},
  1075  							"logging-dir": L{"logging"},
  1076  						},
  1077  					},
  1078  					"mysql": M{
  1079  						"charm":   "cs:quantal/mysql-1",
  1080  						"exposed": true,
  1081  						"units": M{
  1082  							"mysql/0": M{
  1083  								"machine":     "2",
  1084  								"agent-state": "started",
  1085  								"subordinates": M{
  1086  									"logging/1": M{
  1087  										"agent-state":      "error",
  1088  										"agent-state-info": "somehow lost in all those logs",
  1089  									},
  1090  								},
  1091  								"public-address": "dummyenv-2.dns",
  1092  							},
  1093  						},
  1094  						"relations": M{
  1095  							"server":    L{"wordpress"},
  1096  							"juju-info": L{"logging"},
  1097  						},
  1098  					},
  1099  					"logging": M{
  1100  						"charm":   "cs:quantal/logging-1",
  1101  						"exposed": true,
  1102  						"relations": M{
  1103  							"logging-directory": L{"wordpress"},
  1104  							"info":              L{"mysql"},
  1105  						},
  1106  						"subordinate-to": L{"mysql", "wordpress"},
  1107  					},
  1108  				},
  1109  			},
  1110  		},
  1111  
  1112  		// scoped on wordpress/0
  1113  		scopedExpect{
  1114  			"subordinates scoped on logging",
  1115  			[]string{"wordpress/0"},
  1116  			M{
  1117  				"environment": "dummyenv",
  1118  				"machines": M{
  1119  					"1": machine1,
  1120  				},
  1121  				"services": M{
  1122  					"wordpress": M{
  1123  						"charm":   "cs:quantal/wordpress-3",
  1124  						"exposed": true,
  1125  						"units": M{
  1126  							"wordpress/0": M{
  1127  								"machine":     "1",
  1128  								"agent-state": "started",
  1129  								"subordinates": M{
  1130  									"logging/0": M{
  1131  										"agent-state": "started",
  1132  									},
  1133  								},
  1134  								"public-address": "dummyenv-1.dns",
  1135  							},
  1136  						},
  1137  						"relations": M{
  1138  							"db":          L{"mysql"},
  1139  							"logging-dir": L{"logging"},
  1140  						},
  1141  					},
  1142  					"logging": M{
  1143  						"charm":   "cs:quantal/logging-1",
  1144  						"exposed": true,
  1145  						"relations": M{
  1146  							"logging-directory": L{"wordpress"},
  1147  							"info":              L{"mysql"},
  1148  						},
  1149  						"subordinate-to": L{"mysql", "wordpress"},
  1150  					},
  1151  				},
  1152  			},
  1153  		},
  1154  	), test(
  1155  		"one service with two subordinate services",
  1156  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1157  		startAliveMachine{"0"},
  1158  		setMachineStatus{"0", params.StatusStarted, ""},
  1159  		addCharm{"wordpress"},
  1160  		addCharm{"logging"},
  1161  		addCharm{"monitoring"},
  1162  
  1163  		addService{"wordpress", "wordpress"},
  1164  		setServiceExposed{"wordpress", true},
  1165  		addMachine{machineId: "1", job: state.JobHostUnits},
  1166  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1167  		startAliveMachine{"1"},
  1168  		setMachineStatus{"1", params.StatusStarted, ""},
  1169  		addAliveUnit{"wordpress", "1"},
  1170  		setUnitStatus{"wordpress/0", params.StatusStarted, ""},
  1171  
  1172  		addService{"logging", "logging"},
  1173  		setServiceExposed{"logging", true},
  1174  		addService{"monitoring", "monitoring"},
  1175  		setServiceExposed{"monitoring", true},
  1176  
  1177  		relateServices{"wordpress", "logging"},
  1178  		relateServices{"wordpress", "monitoring"},
  1179  
  1180  		addSubordinate{"wordpress/0", "logging"},
  1181  		addSubordinate{"wordpress/0", "monitoring"},
  1182  
  1183  		setUnitsAlive{"logging"},
  1184  		setUnitStatus{"logging/0", params.StatusStarted, ""},
  1185  
  1186  		setUnitsAlive{"monitoring"},
  1187  		setUnitStatus{"monitoring/0", params.StatusStarted, ""},
  1188  
  1189  		// scoped on monitoring; make sure logging doesn't show up.
  1190  		scopedExpect{
  1191  			"subordinates scoped on:",
  1192  			[]string{"monitoring"},
  1193  			M{
  1194  				"environment": "dummyenv",
  1195  				"machines": M{
  1196  					"1": machine1,
  1197  				},
  1198  				"services": M{
  1199  					"wordpress": M{
  1200  						"charm":   "cs:quantal/wordpress-3",
  1201  						"exposed": true,
  1202  						"units": M{
  1203  							"wordpress/0": M{
  1204  								"machine":     "1",
  1205  								"agent-state": "started",
  1206  								"subordinates": M{
  1207  									"monitoring/0": M{
  1208  										"agent-state": "started",
  1209  									},
  1210  								},
  1211  								"public-address": "dummyenv-1.dns",
  1212  							},
  1213  						},
  1214  						"relations": M{
  1215  							"logging-dir":     L{"logging"},
  1216  							"monitoring-port": L{"monitoring"},
  1217  						},
  1218  					},
  1219  					"monitoring": M{
  1220  						"charm":   "cs:quantal/monitoring-0",
  1221  						"exposed": true,
  1222  						"relations": M{
  1223  							"monitoring-port": L{"wordpress"},
  1224  						},
  1225  						"subordinate-to": L{"wordpress"},
  1226  					},
  1227  				},
  1228  			},
  1229  		},
  1230  	), test(
  1231  		"machines with containers",
  1232  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1233  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
  1234  		startAliveMachine{"0"},
  1235  		setMachineStatus{"0", params.StatusStarted, ""},
  1236  		addCharm{"mysql"},
  1237  		addService{"mysql", "mysql"},
  1238  		setServiceExposed{"mysql", true},
  1239  
  1240  		addMachine{machineId: "1", job: state.JobHostUnits},
  1241  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1242  		startAliveMachine{"1"},
  1243  		setMachineStatus{"1", params.StatusStarted, ""},
  1244  		addAliveUnit{"mysql", "1"},
  1245  		setUnitStatus{"mysql/0", params.StatusStarted, ""},
  1246  
  1247  		// A container on machine 1.
  1248  		addContainer{"1", "1/lxc/0", state.JobHostUnits},
  1249  		setAddresses{"1/lxc/0", []instance.Address{instance.NewAddress("dummyenv-2.dns")}},
  1250  		startAliveMachine{"1/lxc/0"},
  1251  		setMachineStatus{"1/lxc/0", params.StatusStarted, ""},
  1252  		addAliveUnit{"mysql", "1/lxc/0"},
  1253  		setUnitStatus{"mysql/1", params.StatusStarted, ""},
  1254  		addContainer{"1", "1/lxc/1", state.JobHostUnits},
  1255  
  1256  		// A nested container.
  1257  		addContainer{"1/lxc/0", "1/lxc/0/lxc/0", state.JobHostUnits},
  1258  		setAddresses{"1/lxc/0/lxc/0", []instance.Address{instance.NewAddress("dummyenv-3.dns")}},
  1259  		startAliveMachine{"1/lxc/0/lxc/0"},
  1260  		setMachineStatus{"1/lxc/0/lxc/0", params.StatusStarted, ""},
  1261  
  1262  		expect{
  1263  			"machines with nested containers",
  1264  			M{
  1265  				"environment": "dummyenv",
  1266  				"machines": M{
  1267  					"0": machine0,
  1268  					"1": machine1WithContainers,
  1269  				},
  1270  				"services": M{
  1271  					"mysql": M{
  1272  						"charm":   "cs:quantal/mysql-1",
  1273  						"exposed": true,
  1274  						"units": M{
  1275  							"mysql/0": M{
  1276  								"machine":        "1",
  1277  								"agent-state":    "started",
  1278  								"public-address": "dummyenv-1.dns",
  1279  							},
  1280  							"mysql/1": M{
  1281  								"machine":        "1/lxc/0",
  1282  								"agent-state":    "started",
  1283  								"public-address": "dummyenv-2.dns",
  1284  							},
  1285  						},
  1286  					},
  1287  				},
  1288  			},
  1289  		},
  1290  
  1291  		// once again, with a scope on mysql/1
  1292  		scopedExpect{
  1293  			"machines with nested containers",
  1294  			[]string{"mysql/1"},
  1295  			M{
  1296  				"environment": "dummyenv",
  1297  				"machines": M{
  1298  					"1": machine1WithContainersScoped,
  1299  				},
  1300  				"services": M{
  1301  					"mysql": M{
  1302  						"charm":   "cs:quantal/mysql-1",
  1303  						"exposed": true,
  1304  						"units": M{
  1305  							"mysql/1": M{
  1306  								"machine":        "1/lxc/0",
  1307  								"agent-state":    "started",
  1308  								"public-address": "dummyenv-2.dns",
  1309  							},
  1310  						},
  1311  					},
  1312  				},
  1313  			},
  1314  		},
  1315  	), test(
  1316  		"service with out of date charm",
  1317  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1318  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
  1319  		startAliveMachine{"0"},
  1320  		setMachineStatus{"0", params.StatusStarted, ""},
  1321  		addMachine{machineId: "1", job: state.JobHostUnits},
  1322  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1323  		startAliveMachine{"1"},
  1324  		setMachineStatus{"1", params.StatusStarted, ""},
  1325  		addCharm{"mysql"},
  1326  		addService{"mysql", "mysql"},
  1327  		setServiceExposed{"mysql", true},
  1328  		addCharmPlaceholder{"mysql", 23},
  1329  		addAliveUnit{"mysql", "1"},
  1330  
  1331  		expect{
  1332  			"services and units with correct charm status",
  1333  			M{
  1334  				"environment": "dummyenv",
  1335  				"machines": M{
  1336  					"0": machine0,
  1337  					"1": machine1,
  1338  				},
  1339  				"services": M{
  1340  					"mysql": M{
  1341  						"charm":          "cs:quantal/mysql-1",
  1342  						"can-upgrade-to": "cs:quantal/mysql-23",
  1343  						"exposed":        true,
  1344  						"units": M{
  1345  							"mysql/0": M{
  1346  								"machine":        "1",
  1347  								"agent-state":    "pending",
  1348  								"public-address": "dummyenv-1.dns",
  1349  							},
  1350  						},
  1351  					},
  1352  				},
  1353  			},
  1354  		},
  1355  	), test(
  1356  		"unit with out of date charm",
  1357  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1358  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
  1359  		startAliveMachine{"0"},
  1360  		setMachineStatus{"0", params.StatusStarted, ""},
  1361  		addMachine{machineId: "1", job: state.JobHostUnits},
  1362  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1363  		startAliveMachine{"1"},
  1364  		setMachineStatus{"1", params.StatusStarted, ""},
  1365  		addCharm{"mysql"},
  1366  		addService{"mysql", "mysql"},
  1367  		setServiceExposed{"mysql", true},
  1368  		addAliveUnit{"mysql", "1"},
  1369  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1370  		addCharmWithRevision{addCharm{"mysql"}, "local", 1},
  1371  		setServiceCharm{"mysql", "local:quantal/mysql-1"},
  1372  
  1373  		expect{
  1374  			"services and units with correct charm status",
  1375  			M{
  1376  				"environment": "dummyenv",
  1377  				"machines": M{
  1378  					"0": machine0,
  1379  					"1": machine1,
  1380  				},
  1381  				"services": M{
  1382  					"mysql": M{
  1383  						"charm":   "local:quantal/mysql-1",
  1384  						"exposed": true,
  1385  						"units": M{
  1386  							"mysql/0": M{
  1387  								"machine":        "1",
  1388  								"agent-state":    "started",
  1389  								"upgrading-from": "cs:quantal/mysql-1",
  1390  								"public-address": "dummyenv-1.dns",
  1391  							},
  1392  						},
  1393  					},
  1394  				},
  1395  			},
  1396  		},
  1397  	), test(
  1398  		"service and unit with out of date charms",
  1399  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1400  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
  1401  		startAliveMachine{"0"},
  1402  		setMachineStatus{"0", params.StatusStarted, ""},
  1403  		addMachine{machineId: "1", job: state.JobHostUnits},
  1404  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1405  		startAliveMachine{"1"},
  1406  		setMachineStatus{"1", params.StatusStarted, ""},
  1407  		addCharm{"mysql"},
  1408  		addService{"mysql", "mysql"},
  1409  		setServiceExposed{"mysql", true},
  1410  		addAliveUnit{"mysql", "1"},
  1411  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1412  		addCharmWithRevision{addCharm{"mysql"}, "cs", 2},
  1413  		setServiceCharm{"mysql", "cs:quantal/mysql-2"},
  1414  		addCharmPlaceholder{"mysql", 23},
  1415  
  1416  		expect{
  1417  			"services and units with correct charm status",
  1418  			M{
  1419  				"environment": "dummyenv",
  1420  				"machines": M{
  1421  					"0": machine0,
  1422  					"1": machine1,
  1423  				},
  1424  				"services": M{
  1425  					"mysql": M{
  1426  						"charm":          "cs:quantal/mysql-2",
  1427  						"can-upgrade-to": "cs:quantal/mysql-23",
  1428  						"exposed":        true,
  1429  						"units": M{
  1430  							"mysql/0": M{
  1431  								"machine":        "1",
  1432  								"agent-state":    "started",
  1433  								"upgrading-from": "cs:quantal/mysql-1",
  1434  								"public-address": "dummyenv-1.dns",
  1435  							},
  1436  						},
  1437  					},
  1438  				},
  1439  			},
  1440  		},
  1441  	), test(
  1442  		"service with local charm not shown as out of date",
  1443  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1444  		setAddresses{"0", []instance.Address{instance.NewAddress("dummyenv-0.dns")}},
  1445  		startAliveMachine{"0"},
  1446  		setMachineStatus{"0", params.StatusStarted, ""},
  1447  		addMachine{machineId: "1", job: state.JobHostUnits},
  1448  		setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}},
  1449  		startAliveMachine{"1"},
  1450  		setMachineStatus{"1", params.StatusStarted, ""},
  1451  		addCharm{"mysql"},
  1452  		addService{"mysql", "mysql"},
  1453  		setServiceExposed{"mysql", true},
  1454  		addAliveUnit{"mysql", "1"},
  1455  		setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"},
  1456  		addCharmWithRevision{addCharm{"mysql"}, "local", 1},
  1457  		setServiceCharm{"mysql", "local:quantal/mysql-1"},
  1458  		addCharmPlaceholder{"mysql", 23},
  1459  
  1460  		expect{
  1461  			"services and units with correct charm status",
  1462  			M{
  1463  				"environment": "dummyenv",
  1464  				"machines": M{
  1465  					"0": machine0,
  1466  					"1": machine1,
  1467  				},
  1468  				"services": M{
  1469  					"mysql": M{
  1470  						"charm":   "local:quantal/mysql-1",
  1471  						"exposed": true,
  1472  						"units": M{
  1473  							"mysql/0": M{
  1474  								"machine":        "1",
  1475  								"agent-state":    "started",
  1476  								"upgrading-from": "cs:quantal/mysql-1",
  1477  								"public-address": "dummyenv-1.dns",
  1478  							},
  1479  						},
  1480  					},
  1481  				},
  1482  			},
  1483  		},
  1484  	),
  1485  }
  1486  
  1487  // TODO(dfc) test failing components by destructively mutating the state under the hood
  1488  
  1489  type addMachine struct {
  1490  	machineId string
  1491  	cons      constraints.Value
  1492  	job       state.MachineJob
  1493  }
  1494  
  1495  func (am addMachine) step(c *gc.C, ctx *context) {
  1496  	m, err := ctx.st.AddOneMachine(state.MachineTemplate{
  1497  		Series:      "quantal",
  1498  		Constraints: am.cons,
  1499  		Jobs:        []state.MachineJob{am.job},
  1500  	})
  1501  	c.Assert(err, gc.IsNil)
  1502  	c.Assert(m.Id(), gc.Equals, am.machineId)
  1503  }
  1504  
  1505  type addContainer struct {
  1506  	parentId  string
  1507  	machineId string
  1508  	job       state.MachineJob
  1509  }
  1510  
  1511  func (ac addContainer) step(c *gc.C, ctx *context) {
  1512  	template := state.MachineTemplate{
  1513  		Series: "quantal",
  1514  		Jobs:   []state.MachineJob{ac.job},
  1515  	}
  1516  	m, err := ctx.st.AddMachineInsideMachine(template, ac.parentId, instance.LXC)
  1517  	c.Assert(err, gc.IsNil)
  1518  	c.Assert(m.Id(), gc.Equals, ac.machineId)
  1519  }
  1520  
  1521  type startMachine struct {
  1522  	machineId string
  1523  }
  1524  
  1525  func (sm startMachine) step(c *gc.C, ctx *context) {
  1526  	m, err := ctx.st.Machine(sm.machineId)
  1527  	c.Assert(err, gc.IsNil)
  1528  	cons, err := m.Constraints()
  1529  	c.Assert(err, gc.IsNil)
  1530  	inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.conn.Environ, m.Id(), cons)
  1531  	err = m.SetProvisioned(inst.Id(), "fake_nonce", hc)
  1532  	c.Assert(err, gc.IsNil)
  1533  }
  1534  
  1535  type startMissingMachine struct {
  1536  	machineId string
  1537  }
  1538  
  1539  func (sm startMissingMachine) step(c *gc.C, ctx *context) {
  1540  	m, err := ctx.st.Machine(sm.machineId)
  1541  	c.Assert(err, gc.IsNil)
  1542  	cons, err := m.Constraints()
  1543  	c.Assert(err, gc.IsNil)
  1544  	_, hc := testing.AssertStartInstanceWithConstraints(c, ctx.conn.Environ, m.Id(), cons)
  1545  	err = m.SetProvisioned("i-missing", "fake_nonce", hc)
  1546  	c.Assert(err, gc.IsNil)
  1547  	err = m.SetInstanceStatus("missing")
  1548  	c.Assert(err, gc.IsNil)
  1549  }
  1550  
  1551  type startAliveMachine struct {
  1552  	machineId string
  1553  }
  1554  
  1555  func (sam startAliveMachine) step(c *gc.C, ctx *context) {
  1556  	m, err := ctx.st.Machine(sam.machineId)
  1557  	c.Assert(err, gc.IsNil)
  1558  	pinger := ctx.setAgentAlive(c, m)
  1559  	cons, err := m.Constraints()
  1560  	c.Assert(err, gc.IsNil)
  1561  	inst, hc := testing.AssertStartInstanceWithConstraints(c, ctx.conn.Environ, m.Id(), cons)
  1562  	err = m.SetProvisioned(inst.Id(), "fake_nonce", hc)
  1563  	c.Assert(err, gc.IsNil)
  1564  	ctx.pingers[m.Id()] = pinger
  1565  }
  1566  
  1567  type setAddresses struct {
  1568  	machineId string
  1569  	addresses []instance.Address
  1570  }
  1571  
  1572  func (sa setAddresses) step(c *gc.C, ctx *context) {
  1573  	m, err := ctx.st.Machine(sa.machineId)
  1574  	c.Assert(err, gc.IsNil)
  1575  	err = m.SetAddresses(sa.addresses)
  1576  	c.Assert(err, gc.IsNil)
  1577  }
  1578  
  1579  type setTools struct {
  1580  	machineId string
  1581  	version   version.Binary
  1582  }
  1583  
  1584  func (st setTools) step(c *gc.C, ctx *context) {
  1585  	m, err := ctx.st.Machine(st.machineId)
  1586  	c.Assert(err, gc.IsNil)
  1587  	err = m.SetAgentVersion(st.version)
  1588  	c.Assert(err, gc.IsNil)
  1589  }
  1590  
  1591  type addCharm struct {
  1592  	name string
  1593  }
  1594  
  1595  func (ac addCharm) addCharmStep(c *gc.C, ctx *context, scheme string, rev int) {
  1596  	ch := coretesting.Charms.Dir(ac.name)
  1597  	name := ch.Meta().Name
  1598  	curl := charm.MustParseURL(fmt.Sprintf("%s:quantal/%s-%d", scheme, name, rev))
  1599  	bundleURL, err := url.Parse(fmt.Sprintf("http://bundles.testing.invalid/%s-%d", name, rev))
  1600  	c.Assert(err, gc.IsNil)
  1601  	dummy, err := ctx.st.AddCharm(ch, curl, bundleURL, fmt.Sprintf("%s-%d-sha256", name, rev))
  1602  	c.Assert(err, gc.IsNil)
  1603  	ctx.charms[ac.name] = dummy
  1604  }
  1605  
  1606  func (ac addCharm) step(c *gc.C, ctx *context) {
  1607  	ch := coretesting.Charms.Dir(ac.name)
  1608  	ac.addCharmStep(c, ctx, "cs", ch.Revision())
  1609  }
  1610  
  1611  type addCharmWithRevision struct {
  1612  	addCharm
  1613  	scheme string
  1614  	rev    int
  1615  }
  1616  
  1617  func (ac addCharmWithRevision) step(c *gc.C, ctx *context) {
  1618  	ac.addCharmStep(c, ctx, ac.scheme, ac.rev)
  1619  }
  1620  
  1621  type addService struct {
  1622  	name  string
  1623  	charm string
  1624  }
  1625  
  1626  func (as addService) step(c *gc.C, ctx *context) {
  1627  	ch, ok := ctx.charms[as.charm]
  1628  	c.Assert(ok, gc.Equals, true)
  1629  	_, err := ctx.st.AddService(as.name, "user-admin", ch)
  1630  	c.Assert(err, gc.IsNil)
  1631  }
  1632  
  1633  type setServiceExposed struct {
  1634  	name    string
  1635  	exposed bool
  1636  }
  1637  
  1638  func (sse setServiceExposed) step(c *gc.C, ctx *context) {
  1639  	s, err := ctx.st.Service(sse.name)
  1640  	c.Assert(err, gc.IsNil)
  1641  	if sse.exposed {
  1642  		err = s.SetExposed()
  1643  		c.Assert(err, gc.IsNil)
  1644  	}
  1645  }
  1646  
  1647  type setServiceCharm struct {
  1648  	name  string
  1649  	charm string
  1650  }
  1651  
  1652  func (ssc setServiceCharm) step(c *gc.C, ctx *context) {
  1653  	ch, err := ctx.st.Charm(charm.MustParseURL(ssc.charm))
  1654  	c.Assert(err, gc.IsNil)
  1655  	s, err := ctx.st.Service(ssc.name)
  1656  	c.Assert(err, gc.IsNil)
  1657  	err = s.SetCharm(ch, false)
  1658  	c.Assert(err, gc.IsNil)
  1659  }
  1660  
  1661  type addCharmPlaceholder struct {
  1662  	name string
  1663  	rev  int
  1664  }
  1665  
  1666  func (ac addCharmPlaceholder) step(c *gc.C, ctx *context) {
  1667  	ch := coretesting.Charms.Dir(ac.name)
  1668  	name := ch.Meta().Name
  1669  	curl := charm.MustParseURL(fmt.Sprintf("cs:quantal/%s-%d", name, ac.rev))
  1670  	err := ctx.st.AddStoreCharmPlaceholder(curl)
  1671  	c.Assert(err, gc.IsNil)
  1672  }
  1673  
  1674  type addUnit struct {
  1675  	serviceName string
  1676  	machineId   string
  1677  }
  1678  
  1679  func (au addUnit) step(c *gc.C, ctx *context) {
  1680  	s, err := ctx.st.Service(au.serviceName)
  1681  	c.Assert(err, gc.IsNil)
  1682  	u, err := s.AddUnit()
  1683  	c.Assert(err, gc.IsNil)
  1684  	m, err := ctx.st.Machine(au.machineId)
  1685  	c.Assert(err, gc.IsNil)
  1686  	err = u.AssignToMachine(m)
  1687  	c.Assert(err, gc.IsNil)
  1688  }
  1689  
  1690  type addAliveUnit struct {
  1691  	serviceName string
  1692  	machineId   string
  1693  }
  1694  
  1695  func (aau addAliveUnit) step(c *gc.C, ctx *context) {
  1696  	s, err := ctx.st.Service(aau.serviceName)
  1697  	c.Assert(err, gc.IsNil)
  1698  	u, err := s.AddUnit()
  1699  	c.Assert(err, gc.IsNil)
  1700  	pinger := ctx.setAgentAlive(c, u)
  1701  	m, err := ctx.st.Machine(aau.machineId)
  1702  	c.Assert(err, gc.IsNil)
  1703  	err = u.AssignToMachine(m)
  1704  	c.Assert(err, gc.IsNil)
  1705  	ctx.pingers[u.Name()] = pinger
  1706  }
  1707  
  1708  type setUnitsAlive struct {
  1709  	serviceName string
  1710  }
  1711  
  1712  func (sua setUnitsAlive) step(c *gc.C, ctx *context) {
  1713  	s, err := ctx.st.Service(sua.serviceName)
  1714  	c.Assert(err, gc.IsNil)
  1715  	us, err := s.AllUnits()
  1716  	c.Assert(err, gc.IsNil)
  1717  	for _, u := range us {
  1718  		ctx.pingers[u.Name()] = ctx.setAgentAlive(c, u)
  1719  	}
  1720  }
  1721  
  1722  type setUnitStatus struct {
  1723  	unitName   string
  1724  	status     params.Status
  1725  	statusInfo string
  1726  }
  1727  
  1728  func (sus setUnitStatus) step(c *gc.C, ctx *context) {
  1729  	u, err := ctx.st.Unit(sus.unitName)
  1730  	c.Assert(err, gc.IsNil)
  1731  	err = u.SetStatus(sus.status, sus.statusInfo, nil)
  1732  	c.Assert(err, gc.IsNil)
  1733  }
  1734  
  1735  type setUnitCharmURL struct {
  1736  	unitName string
  1737  	charm    string
  1738  }
  1739  
  1740  func (uc setUnitCharmURL) step(c *gc.C, ctx *context) {
  1741  	u, err := ctx.st.Unit(uc.unitName)
  1742  	c.Assert(err, gc.IsNil)
  1743  	curl := charm.MustParseURL(uc.charm)
  1744  	err = u.SetCharmURL(curl)
  1745  	c.Assert(err, gc.IsNil)
  1746  	err = u.SetStatus(params.StatusStarted, "", nil)
  1747  	c.Assert(err, gc.IsNil)
  1748  }
  1749  
  1750  type openUnitPort struct {
  1751  	unitName string
  1752  	protocol string
  1753  	number   int
  1754  }
  1755  
  1756  func (oup openUnitPort) step(c *gc.C, ctx *context) {
  1757  	u, err := ctx.st.Unit(oup.unitName)
  1758  	c.Assert(err, gc.IsNil)
  1759  	err = u.OpenPort(oup.protocol, oup.number)
  1760  	c.Assert(err, gc.IsNil)
  1761  }
  1762  
  1763  type ensureDyingUnit struct {
  1764  	unitName string
  1765  }
  1766  
  1767  func (e ensureDyingUnit) step(c *gc.C, ctx *context) {
  1768  	u, err := ctx.st.Unit(e.unitName)
  1769  	c.Assert(err, gc.IsNil)
  1770  	err = u.Destroy()
  1771  	c.Assert(err, gc.IsNil)
  1772  	c.Assert(u.Life(), gc.Equals, state.Dying)
  1773  }
  1774  
  1775  type ensureDyingService struct {
  1776  	serviceName string
  1777  }
  1778  
  1779  func (e ensureDyingService) step(c *gc.C, ctx *context) {
  1780  	svc, err := ctx.st.Service(e.serviceName)
  1781  	c.Assert(err, gc.IsNil)
  1782  	err = svc.Destroy()
  1783  	c.Assert(err, gc.IsNil)
  1784  	err = svc.Refresh()
  1785  	c.Assert(err, gc.IsNil)
  1786  	c.Assert(svc.Life(), gc.Equals, state.Dying)
  1787  }
  1788  
  1789  type ensureDeadMachine struct {
  1790  	machineId string
  1791  }
  1792  
  1793  func (e ensureDeadMachine) step(c *gc.C, ctx *context) {
  1794  	m, err := ctx.st.Machine(e.machineId)
  1795  	c.Assert(err, gc.IsNil)
  1796  	err = m.EnsureDead()
  1797  	c.Assert(err, gc.IsNil)
  1798  	c.Assert(m.Life(), gc.Equals, state.Dead)
  1799  }
  1800  
  1801  type setMachineStatus struct {
  1802  	machineId  string
  1803  	status     params.Status
  1804  	statusInfo string
  1805  }
  1806  
  1807  func (sms setMachineStatus) step(c *gc.C, ctx *context) {
  1808  	m, err := ctx.st.Machine(sms.machineId)
  1809  	c.Assert(err, gc.IsNil)
  1810  	err = m.SetStatus(sms.status, sms.statusInfo, nil)
  1811  	c.Assert(err, gc.IsNil)
  1812  }
  1813  
  1814  type relateServices struct {
  1815  	ep1, ep2 string
  1816  }
  1817  
  1818  func (rs relateServices) step(c *gc.C, ctx *context) {
  1819  	eps, err := ctx.st.InferEndpoints([]string{rs.ep1, rs.ep2})
  1820  	c.Assert(err, gc.IsNil)
  1821  	_, err = ctx.st.AddRelation(eps...)
  1822  	c.Assert(err, gc.IsNil)
  1823  }
  1824  
  1825  type addSubordinate struct {
  1826  	prinUnit   string
  1827  	subService string
  1828  }
  1829  
  1830  func (as addSubordinate) step(c *gc.C, ctx *context) {
  1831  	u, err := ctx.st.Unit(as.prinUnit)
  1832  	c.Assert(err, gc.IsNil)
  1833  	eps, err := ctx.st.InferEndpoints([]string{u.ServiceName(), as.subService})
  1834  	c.Assert(err, gc.IsNil)
  1835  	rel, err := ctx.st.EndpointsRelation(eps...)
  1836  	c.Assert(err, gc.IsNil)
  1837  	ru, err := rel.Unit(u)
  1838  	c.Assert(err, gc.IsNil)
  1839  	err = ru.EnterScope(nil)
  1840  	c.Assert(err, gc.IsNil)
  1841  }
  1842  
  1843  type scopedExpect struct {
  1844  	what   string
  1845  	scope  []string
  1846  	output M
  1847  }
  1848  
  1849  type expect struct {
  1850  	what   string
  1851  	output M
  1852  }
  1853  
  1854  func (e scopedExpect) step(c *gc.C, ctx *context) {
  1855  	c.Logf("expect: %s %s", e.what, strings.Join(e.scope, " "))
  1856  
  1857  	// Now execute the command for each format.
  1858  	for _, format := range statusFormats {
  1859  		c.Logf("format %q", format.name)
  1860  		// Run command with the required format.
  1861  		args := append([]string{"--format", format.name}, e.scope...)
  1862  		code, stdout, stderr := runStatus(c, args...)
  1863  		c.Assert(code, gc.Equals, 0)
  1864  		if !c.Check(stderr, gc.HasLen, 0) {
  1865  			c.Fatalf("status failed: %s", string(stderr))
  1866  		}
  1867  
  1868  		// Prepare the output in the same format.
  1869  		buf, err := format.marshal(e.output)
  1870  		c.Assert(err, gc.IsNil)
  1871  		expected := make(M)
  1872  		err = format.unmarshal(buf, &expected)
  1873  		c.Assert(err, gc.IsNil)
  1874  
  1875  		// Check the output is as expected.
  1876  		actual := make(M)
  1877  		err = format.unmarshal(stdout, &actual)
  1878  		c.Assert(err, gc.IsNil)
  1879  		c.Assert(actual, gc.DeepEquals, expected)
  1880  	}
  1881  }
  1882  
  1883  func (e expect) step(c *gc.C, ctx *context) {
  1884  	scopedExpect{e.what, nil, e.output}.step(c, ctx)
  1885  }
  1886  
  1887  func (s *StatusSuite) TestStatusAllFormats(c *gc.C) {
  1888  	for i, t := range statusTests {
  1889  		c.Logf("test %d: %s", i, t.summary)
  1890  		func() {
  1891  			// Prepare context and run all steps to setup.
  1892  			ctx := s.newContext()
  1893  			defer s.resetContext(c, ctx)
  1894  			ctx.run(c, t.steps)
  1895  		}()
  1896  	}
  1897  }
  1898  
  1899  func (s *StatusSuite) TestStatusFilterErrors(c *gc.C) {
  1900  	steps := []stepper{
  1901  		addMachine{machineId: "0", job: state.JobManageEnviron},
  1902  		addMachine{machineId: "1", job: state.JobHostUnits},
  1903  		addCharm{"mysql"},
  1904  		addService{"mysql", "mysql"},
  1905  		addAliveUnit{"mysql", "1"},
  1906  	}
  1907  	ctx := s.newContext()
  1908  	defer s.resetContext(c, ctx)
  1909  	ctx.run(c, steps)
  1910  
  1911  	// Status filters can only fail if the patterns are invalid.
  1912  	code, _, stderr := runStatus(c, "[*")
  1913  	c.Assert(code, gc.Not(gc.Equals), 0)
  1914  	c.Assert(string(stderr), gc.Equals, `error: pattern "[*" contains invalid characters`+"\n")
  1915  
  1916  	code, _, stderr = runStatus(c, "//")
  1917  	c.Assert(code, gc.Not(gc.Equals), 0)
  1918  	c.Assert(string(stderr), gc.Equals, `error: pattern "//" contains too many '/' characters`+"\n")
  1919  
  1920  	// Pattern validity is checked eagerly; if a bad pattern
  1921  	// proceeds a valid, matching pattern, then the bad pattern
  1922  	// will still cause an error.
  1923  	code, _, stderr = runStatus(c, "*", "[*")
  1924  	c.Assert(code, gc.Not(gc.Equals), 0)
  1925  	c.Assert(string(stderr), gc.Equals, `error: pattern "[*" contains invalid characters`+"\n")
  1926  }