github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/juju/run_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package main 5 6 import ( 7 "fmt" 8 "time" 9 10 jc "github.com/juju/testing/checkers" 11 "github.com/juju/utils/exec" 12 gc "launchpad.net/gocheck" 13 14 "github.com/juju/juju/cmd" 15 "github.com/juju/juju/cmd/envcmd" 16 "github.com/juju/juju/state/api/params" 17 "github.com/juju/juju/testing" 18 ) 19 20 type RunSuite struct { 21 testing.FakeJujuHomeSuite 22 } 23 24 var _ = gc.Suite(&RunSuite{}) 25 26 func (*RunSuite) TestTargetArgParsing(c *gc.C) { 27 for i, test := range []struct { 28 message string 29 args []string 30 all bool 31 machines []string 32 units []string 33 services []string 34 commands string 35 errMatch string 36 }{{ 37 message: "no args", 38 errMatch: "no commands specified", 39 }, { 40 message: "no target", 41 args: []string{"sudo reboot"}, 42 errMatch: "You must specify a target, either through --all, --machine, --service or --unit", 43 }, { 44 message: "too many args", 45 args: []string{"--all", "sudo reboot", "oops"}, 46 errMatch: `unrecognized args: \["oops"\]`, 47 }, { 48 message: "command to all machines", 49 args: []string{"--all", "sudo reboot"}, 50 all: true, 51 commands: "sudo reboot", 52 }, { 53 message: "all and defined machines", 54 args: []string{"--all", "--machine=1,2", "sudo reboot"}, 55 errMatch: `You cannot specify --all and individual machines`, 56 }, { 57 message: "command to machines 1, 2, and 1/kvm/0", 58 args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, 59 commands: "sudo reboot", 60 machines: []string{"1", "2", "1/kvm/0"}, 61 }, { 62 message: "bad machine names", 63 args: []string{"--machine=foo,machine-2", "sudo reboot"}, 64 errMatch: "" + 65 "The following run targets are not valid:\n" + 66 " \"foo\" is not a valid machine id\n" + 67 " \"machine-2\" is not a valid machine id", 68 }, { 69 message: "all and defined services", 70 args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"}, 71 errMatch: `You cannot specify --all and individual services`, 72 }, { 73 message: "command to services wordpress and mysql", 74 args: []string{"--service=wordpress,mysql", "sudo reboot"}, 75 commands: "sudo reboot", 76 services: []string{"wordpress", "mysql"}, 77 }, { 78 message: "bad service names", 79 args: []string{"--service", "foo,2,foo/0", "sudo reboot"}, 80 errMatch: "" + 81 "The following run targets are not valid:\n" + 82 " \"2\" is not a valid service name\n" + 83 " \"foo/0\" is not a valid service name", 84 }, { 85 message: "all and defined units", 86 args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, 87 errMatch: `You cannot specify --all and individual units`, 88 }, { 89 message: "command to valid units", 90 args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, 91 commands: "sudo reboot", 92 units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, 93 }, { 94 message: "bad unit names", 95 args: []string{"--unit", "foo,2,foo/0", "sudo reboot"}, 96 errMatch: "" + 97 "The following run targets are not valid:\n" + 98 " \"foo\" is not a valid unit name\n" + 99 " \"2\" is not a valid unit name", 100 }, { 101 message: "command to mixed valid targets", 102 args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"}, 103 commands: "sudo reboot", 104 machines: []string{"0"}, 105 services: []string{"mysql"}, 106 units: []string{"wordpress/0", "wordpress/1"}, 107 }} { 108 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 109 runCmd := &RunCommand{} 110 testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) 111 if test.errMatch == "" { 112 c.Check(runCmd.all, gc.Equals, test.all) 113 c.Check(runCmd.machines, gc.DeepEquals, test.machines) 114 c.Check(runCmd.services, gc.DeepEquals, test.services) 115 c.Check(runCmd.units, gc.DeepEquals, test.units) 116 c.Check(runCmd.commands, gc.Equals, test.commands) 117 } 118 } 119 } 120 121 func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { 122 for i, test := range []struct { 123 message string 124 args []string 125 errMatch string 126 timeout time.Duration 127 }{{ 128 message: "default time", 129 args: []string{"--all", "sudo reboot"}, 130 timeout: 5 * time.Minute, 131 }, { 132 message: "invalid time", 133 args: []string{"--timeout=foo", "--all", "sudo reboot"}, 134 errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`, 135 }, { 136 message: "two hours", 137 args: []string{"--timeout=2h", "--all", "sudo reboot"}, 138 timeout: 2 * time.Hour, 139 }, { 140 message: "3 minutes 30 seconds", 141 args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, 142 timeout: (3 * time.Minute) + (30 * time.Second), 143 }} { 144 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 145 runCmd := &RunCommand{} 146 testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch) 147 if test.errMatch == "" { 148 c.Check(runCmd.timeout, gc.Equals, test.timeout) 149 } 150 } 151 } 152 153 func (s *RunSuite) TestConvertRunResults(c *gc.C) { 154 for i, test := range []struct { 155 message string 156 results []params.RunResult 157 expected interface{} 158 }{{ 159 message: "empty", 160 expected: []interface{}{}, 161 }, { 162 message: "minimum is machine id and stdout", 163 results: []params.RunResult{ 164 makeRunResult(mockResponse{machineId: "1"}), 165 }, 166 expected: []interface{}{ 167 map[string]interface{}{ 168 "MachineId": "1", 169 "Stdout": "", 170 }}, 171 }, { 172 message: "other fields are copied if there", 173 results: []params.RunResult{ 174 makeRunResult(mockResponse{ 175 machineId: "1", 176 stdout: "stdout", 177 stderr: "stderr", 178 code: 42, 179 unitId: "unit/0", 180 error: "error", 181 }), 182 }, 183 expected: []interface{}{ 184 map[string]interface{}{ 185 "MachineId": "1", 186 "Stdout": "stdout", 187 "Stderr": "stderr", 188 "ReturnCode": 42, 189 "UnitId": "unit/0", 190 "Error": "error", 191 }}, 192 }, { 193 message: "stdout and stderr are base64 encoded if not valid utf8", 194 results: []params.RunResult{ 195 params.RunResult{ 196 ExecResponse: exec.ExecResponse{ 197 Stdout: []byte{0xff}, 198 Stderr: []byte{0xfe}, 199 }, 200 MachineId: "jake", 201 }, 202 }, 203 expected: []interface{}{ 204 map[string]interface{}{ 205 "MachineId": "jake", 206 "Stdout": "/w==", 207 "Stdout.encoding": "base64", 208 "Stderr": "/g==", 209 "Stderr.encoding": "base64", 210 }}, 211 }, { 212 message: "more than one", 213 results: []params.RunResult{ 214 makeRunResult(mockResponse{machineId: "1"}), 215 makeRunResult(mockResponse{machineId: "2"}), 216 makeRunResult(mockResponse{machineId: "3"}), 217 }, 218 expected: []interface{}{ 219 map[string]interface{}{ 220 "MachineId": "1", 221 "Stdout": "", 222 }, 223 map[string]interface{}{ 224 "MachineId": "2", 225 "Stdout": "", 226 }, 227 map[string]interface{}{ 228 "MachineId": "3", 229 "Stdout": "", 230 }, 231 }, 232 }} { 233 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 234 result := ConvertRunResults(test.results) 235 c.Check(result, jc.DeepEquals, test.expected) 236 } 237 } 238 239 func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { 240 mock := s.setupMockAPI() 241 machineResponse := mockResponse{ 242 stdout: "megatron\n", 243 machineId: "0", 244 } 245 unitResponse := mockResponse{ 246 stdout: "bumblebee", 247 machineId: "1", 248 unitId: "unit/0", 249 } 250 mock.setResponse("0", machineResponse) 251 mock.setResponse("unit/0", unitResponse) 252 253 unformatted := ConvertRunResults([]params.RunResult{ 254 makeRunResult(machineResponse), 255 makeRunResult(unitResponse), 256 }) 257 258 jsonFormatted, err := cmd.FormatJson(unformatted) 259 c.Assert(err, gc.IsNil) 260 261 context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), 262 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 263 ) 264 c.Assert(err, gc.IsNil) 265 266 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 267 } 268 269 func (s *RunSuite) TestAllMachines(c *gc.C) { 270 mock := s.setupMockAPI() 271 mock.setMachinesAlive("0", "1") 272 response0 := mockResponse{ 273 stdout: "megatron\n", 274 machineId: "0", 275 } 276 response1 := mockResponse{ 277 error: "command timed out", 278 machineId: "1", 279 } 280 mock.setResponse("0", response0) 281 282 unformatted := ConvertRunResults([]params.RunResult{ 283 makeRunResult(response0), 284 makeRunResult(response1), 285 }) 286 287 jsonFormatted, err := cmd.FormatJson(unformatted) 288 c.Assert(err, gc.IsNil) 289 290 context, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname") 291 c.Assert(err, gc.IsNil) 292 293 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 294 } 295 296 func (s *RunSuite) TestSingleResponse(c *gc.C) { 297 mock := s.setupMockAPI() 298 mock.setMachinesAlive("0") 299 mockResponse := mockResponse{ 300 stdout: "stdout\n", 301 stderr: "stderr\n", 302 code: 42, 303 machineId: "0", 304 } 305 mock.setResponse("0", mockResponse) 306 unformatted := ConvertRunResults([]params.RunResult{ 307 makeRunResult(mockResponse)}) 308 yamlFormatted, err := cmd.FormatYaml(unformatted) 309 c.Assert(err, gc.IsNil) 310 jsonFormatted, err := cmd.FormatJson(unformatted) 311 c.Assert(err, gc.IsNil) 312 313 for i, test := range []struct { 314 message string 315 format string 316 stdout string 317 stderr string 318 errorMatch string 319 }{{ 320 message: "smart (default)", 321 stdout: "stdout\n", 322 stderr: "stderr\n", 323 errorMatch: "subprocess encountered error code 42", 324 }, { 325 message: "yaml output", 326 format: "yaml", 327 stdout: string(yamlFormatted) + "\n", 328 }, { 329 message: "json output", 330 format: "json", 331 stdout: string(jsonFormatted) + "\n", 332 }} { 333 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 334 args := []string{} 335 if test.format != "" { 336 args = append(args, "--format", test.format) 337 } 338 args = append(args, "--all", "ignored") 339 context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), args...) 340 if test.errorMatch != "" { 341 c.Check(err, gc.ErrorMatches, test.errorMatch) 342 } else { 343 c.Check(err, gc.IsNil) 344 } 345 c.Check(testing.Stdout(context), gc.Equals, test.stdout) 346 c.Check(testing.Stderr(context), gc.Equals, test.stderr) 347 } 348 } 349 350 func (s *RunSuite) setupMockAPI() *mockRunAPI { 351 mock := &mockRunAPI{} 352 s.PatchValue(&getAPIClient, func(name string) (RunClient, error) { 353 return mock, nil 354 }) 355 return mock 356 } 357 358 type mockRunAPI struct { 359 stdout string 360 stderr string 361 code int 362 // machines, services, units 363 machines map[string]bool 364 responses map[string]params.RunResult 365 } 366 367 type mockResponse struct { 368 stdout string 369 stderr string 370 code int 371 error string 372 machineId string 373 unitId string 374 } 375 376 var _ RunClient = (*mockRunAPI)(nil) 377 378 func (m *mockRunAPI) setMachinesAlive(ids ...string) { 379 if m.machines == nil { 380 m.machines = make(map[string]bool) 381 } 382 for _, id := range ids { 383 m.machines[id] = true 384 } 385 } 386 387 func makeRunResult(mock mockResponse) params.RunResult { 388 return params.RunResult{ 389 ExecResponse: exec.ExecResponse{ 390 Stdout: []byte(mock.stdout), 391 Stderr: []byte(mock.stderr), 392 Code: mock.code, 393 }, 394 MachineId: mock.machineId, 395 UnitId: mock.unitId, 396 Error: mock.error, 397 } 398 } 399 400 func (m *mockRunAPI) setResponse(id string, mock mockResponse) { 401 if m.responses == nil { 402 m.responses = make(map[string]params.RunResult) 403 } 404 m.responses[id] = makeRunResult(mock) 405 } 406 407 func (*mockRunAPI) Close() error { 408 return nil 409 } 410 411 func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { 412 var result []params.RunResult 413 for machine := range m.machines { 414 response, found := m.responses[machine] 415 if !found { 416 // Consider this a timeout 417 response = params.RunResult{MachineId: machine, Error: "command timed out"} 418 } 419 result = append(result, response) 420 } 421 422 return result, nil 423 } 424 425 func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) { 426 var result []params.RunResult 427 // Just add in ids that match in order. 428 for _, id := range runParams.Machines { 429 response, found := m.responses[id] 430 if found { 431 result = append(result, response) 432 } 433 } 434 // mock ignores services 435 for _, id := range runParams.Units { 436 response, found := m.responses[id] 437 if found { 438 result = append(result, response) 439 } 440 } 441 442 return result, nil 443 }