PageRenderTime 52ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/cmd/juju/run_test.go

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