Merge lp:~bcsaller/pyjuju/sane_output_test_option into lp:pyjuju

Proposed by Benjamin Saller
Status: Work in progress
Proposed branch: lp:~bcsaller/pyjuju/sane_output_test_option
Merge into: lp:pyjuju
Diff against target: 226 lines (+135/-3)
2 files modified
juju/hooks/commands.py (+37/-3)
juju/hooks/tests/test_invoker.py (+98/-0)
To merge this branch: bzr merge lp:~bcsaller/pyjuju/sane_output_test_option
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+106067@code.launchpad.net

Description of the change

Adds support for --test to cli utils

This branch bundles two related changes, support for --test on relation-get, unit-get and config-get.
This should be similar to the additions made in the golang version.

Also this changes to always use a formatter for output and the smart output formatter was changed to
YAML dump collection types, normal string values are still printed w/o serialization.

https://codereview.appspot.com/6216053/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+106067_code.launchpad.net,

Message:
Please take a look.

Description:
Adds support for --test and doesn't output Python reprs

This branch bundles two related changes, support for --test on
relation-get, unit-get and config-get.
This should be similar to the additions made in the golang version.

Also this changes to always use a formatter for output and the smart
output formatter was changed to
YAML dump collection types, normal string values are still printed w/o
serialization.

https://code.launchpad.net/~bcsaller/juju/sane_output_test_option/+merge/106067

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6216053/

Affected files:
   A [revision details]
   M juju/hooks/cli.py
   M juju/hooks/commands.py
   M juju/hooks/tests/test_invoker.py

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Some details here are not clear to me. I'd appreciate having a chat
about this feature with you before this goes in.

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py
File juju/hooks/cli.py (right):

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py#newcode228
juju/hooks/cli.py:228: # yaml dump container types
What was your thinking here? I believe it should dump yaml in all
cases. i.e. "true", not "True".

https://codereview.appspot.com/6216053/diff/1/juju/hooks/commands.py
File juju/hooks/commands.py (right):

https://codereview.appspot.com/6216053/diff/1/juju/hooks/commands.py#newcode80
juju/hooks/commands.py:80: v = json.dumps(v)
json?

https://codereview.appspot.com/6216053/

Revision history for this message
Clint Byrum (clint-fewbar) wrote :

I've been thinking about this, and this is a backward incompatible change, I'm afraid.

Up until now, charms w/ boolean configs have been able to count on "True" or "False" and have been written as such.

This will change the case, and that is, unfortunately, not acceptable in the planned "fixes only" release of Juju.

The --test is fantastic, and should be added. But I'm afraid if we do the other bit without bumping to a new "major release" we will have many many broken charms very suddenly.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

its backwards incompatible for charms, which we should fix and encourage extant users to update. the existing output is broken in that it depends on no standard except the implicit one by python's str/repr output. The whole 'smart' format stuff was decidedly not.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Mail sent to canonical-juju. This bug will be fixed, we have no option. The decision of when to fix it is your hands, Clint.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

bug in base formatter.

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py
File juju/hooks/cli.py (right):

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py#newcode220
juju/hooks/cli.py:220: formatter(result, stream)
None is not a callable type, this tries a double output if formatter is
None. please default formatter to the correct default output.

https://codereview.appspot.com/6216053/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

it would be nice to rebase this on current trunk where the formatting work has been done in a compatible fashion, and slim this branch down to just the --test option which is still useful

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Please take a look.

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py
File juju/hooks/cli.py (right):

https://codereview.appspot.com/6216053/diff/1/juju/hooks/cli.py#newcode228
juju/hooks/cli.py:228: # yaml dump container types
On 2012/05/17 02:42:44, niemeyer wrote:
> What was your thinking here? I believe it should dump yaml in all
cases. i.e.
> "true", not "True".

You're correct that non-string types should appear here.

https://codereview.appspot.com/6216053/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

I'm still a little unclear as to the desired behavior outcome and its
testing with this branch, as it currently tested it doesn't seem
particularly useful.

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/commands.py
File juju/hooks/commands.py (right):

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/commands.py#newcode18
juju/hooks/commands.py:18: class TestOptionMixin(object):
missing doc string describing purpose/usage.

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/commands.py#newcode34
juju/hooks/commands.py:34: self.handle_test(result)
there's a lot of unesc. method delegation here. just inline the
test_true/handle_test behavior.

The use of the mixin, also feels like its adding complexity here, around
subclass ordering and proper lineage to customize_parser vs. render
(which have inverse call lineages). Incorporating into the base class
might be cleaner via class flag switch.

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/commands.py#newcode114
juju/hooks/commands.py:114: def render(self, result):
Why is this returning None, why is it implemented at all? ditto for the
two other implementations of the same here.

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/tests/test_invoker.py
File juju/hooks/tests/test_invoker.py (right):

https://codereview.appspot.com/6216053/diff/6001/juju/hooks/tests/test_invoker.py#newcode356
juju/hooks/tests/test_invoker.py:356:
this is testing something very different then what the cli usage would
commonly be, namely the cli usage would be testing for values unset.

For values set, a null value seems valid, has the go team documented the
behavior of this cli param?

https://codereview.appspot.com/6216053/

Unmerged revisions

556. By Benjamin Saller

test option support in cli hooks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/hooks/commands.py'
--- juju/hooks/commands.py 2012-05-03 16:46:40 +0000
+++ juju/hooks/commands.py 2012-07-03 18:49:20 +0000
@@ -1,3 +1,4 @@
1import json
1import logging2import logging
2import os3import os
3import pipes4import pipes
@@ -14,7 +15,28 @@
14BAD_CHARS = re.compile("[\-\./:()<>|?*]|(\\\)")15BAD_CHARS = re.compile("[\-\./:()<>|?*]|(\\\)")
1516
1617
17class RelationGetCli(CommandLineClient):18class TestOptionMixin(object):
19 def customize_parser(self):
20 self.parser.add_argument("--test",
21 action="store_true",
22 default=False,
23 help="Exit code indicates if the value "
24 "is present and not empty")
25
26 def tests_true(self, result):
27 return 0 if bool(result) else 1
28
29 def handle_test(self, result):
30 self.exit_code = self.tests_true(result)
31
32 def render(self, result):
33 if self.options.test is True:
34 self.handle_test(result)
35 else:
36 super(TestOptionMixin, self).render(result)
37
38
39class RelationGetCli(TestOptionMixin, CommandLineClient):
18 keyvalue_pairs = False40 keyvalue_pairs = False
1941
20 def customize_parser(self):42 def customize_parser(self):
@@ -24,6 +46,7 @@
24 "-r", dest="relation_id", default="", metavar="RELATION ID")46 "-r", dest="relation_id", default="", metavar="RELATION ID")
25 self.parser.add_argument("settings_name", default="", nargs="?")47 self.parser.add_argument("settings_name", default="", nargs="?")
26 self.parser.add_argument("unit_name", default=remote_unit, nargs="?")48 self.parser.add_argument("unit_name", default=remote_unit, nargs="?")
49 super(RelationGetCli, self).customize_parser()
2750
28 @inlineCallbacks51 @inlineCallbacks
29 def run(self):52 def run(self):
@@ -88,6 +111,9 @@
88 self.options.relation_id,111 self.options.relation_id,
89 self.options.keyvalue_pairs)112 self.options.keyvalue_pairs)
90113
114 def render(self, result):
115 return None
116
91117
92def relation_set():118def relation_set():
93 """Entry point for relation-set."""119 """Entry point for relation-set."""
@@ -179,11 +205,12 @@
179 sys.exit(client())205 sys.exit(client())
180206
181207
182class ConfigGetCli(CommandLineClient):208class ConfigGetCli(TestOptionMixin, CommandLineClient):
183 keyvalue_pairs = False209 keyvalue_pairs = False
184210
185 def customize_parser(self):211 def customize_parser(self):
186 self.parser.add_argument("option_name", default="", nargs="?")212 self.parser.add_argument("option_name", default="", nargs="?")
213 super(ConfigGetCli, self).customize_parser()
187214
188 @inlineCallbacks215 @inlineCallbacks
189 def run(self):216 def run(self):
@@ -213,6 +240,9 @@
213 return self.client.open_port(240 return self.client.open_port(
214 self.options.client_id, *self.options.port_protocol)241 self.options.client_id, *self.options.port_protocol)
215242
243 def render(self, result):
244 return None
245
216246
217def open_port():247def open_port():
218 """Entry point for open-port."""248 """Entry point for open-port."""
@@ -234,6 +264,9 @@
234 return self.client.close_port(264 return self.client.close_port(
235 self.options.client_id, *self.options.port_protocol)265 self.options.client_id, *self.options.port_protocol)
236266
267 def render(self, result):
268 return None
269
237270
238def close_port():271def close_port():
239 """Entry point for close-port."""272 """Entry point for close-port."""
@@ -241,11 +274,12 @@
241 sys.exit(client())274 sys.exit(client())
242275
243276
244class UnitGetCli(CommandLineClient):277class UnitGetCli(TestOptionMixin, CommandLineClient):
245 keyvalue_pairs = False278 keyvalue_pairs = False
246279
247 def customize_parser(self):280 def customize_parser(self):
248 self.parser.add_argument("setting_name")281 self.parser.add_argument("setting_name")
282 super(UnitGetCli, self).customize_parser()
249283
250 @inlineCallbacks284 @inlineCallbacks
251 def run(self):285 def run(self):
252286
=== modified file 'juju/hooks/tests/test_invoker.py'
--- juju/hooks/tests/test_invoker.py 2012-07-03 07:30:53 +0000
+++ juju/hooks/tests/test_invoker.py 2012-07-03 18:49:20 +0000
@@ -340,6 +340,49 @@
340 self.assertEqual('VAR_NAME=rabbit\n\n', data)340 self.assertEqual('VAR_NAME=rabbit\n\n', data)
341341
342 @defer.inlineCallbacks342 @defer.inlineCallbacks
343 def test_relation_get_test_option(self):
344 yield self.build_default_relationships()
345
346 hook_log = self.capture_logging("hook")
347
348 # Populate and verify some data we will
349 # later extract with the hook
350 values = {"string": "rabbit hole",
351 "emptylist": [],
352 "emptystring": "",
353 "zero": 0,
354 "number": 1}
355 yield self.wordpress_relation.set_data(values)
356
357 exe = yield self.ua.get_invoker(
358 "db:42", "add", "mysql/0", self.mysql_relation,
359 client_id="client_id")
360 exe.environment["JUJU_REMOTE_UNIT"] = "wordpress/0"
361
362 @defer.inlineCallbacks
363 def test_exit(variable, expected):
364 # --test can return a bad exit code from the POV of
365 # the invoker, this is fine, its just the way I'm using
366 # it to get at the value thats otherwise fine in the
367 # hook itself
368 try:
369 result = yield exe(self.create_hook("relation-get",
370 "--test %s" % variable))
371 self.assertEqual(result, expected)
372 except errors.CharmInvocationError:
373 if expected == 1:
374 pass
375 data = hook_log.getvalue()
376 self.assertEqual(data, "")
377
378 yield test_exit("string", 0)
379 yield test_exit("emptylist", 1)
380 yield test_exit("emptystring", 1)
381 yield test_exit("zero", 1)
382 yield test_exit("number", 0)
383
384
385 @defer.inlineCallbacks
343 def test_relation_get_format_shell_bad_vars(self):386 def test_relation_get_format_shell_bad_vars(self):
344 """If illegal values are make somehow available warn."""387 """If illegal values are make somehow available warn."""
345 yield self.build_default_relationships()388 yield self.build_default_relationships()
@@ -433,6 +476,49 @@
433 self.assertEqual(data["name"], "rabbit")476 self.assertEqual(data["name"], "rabbit")
434 self.assertEqual(data["forgotten"], "lyrics")477 self.assertEqual(data["forgotten"], "lyrics")
435478
479 @defer.inlineCallbacks
480 def test_config_get_test_option(self):
481 """Validate that config-get returns expected values."""
482 yield self.build_default_relationships()
483
484 hook_log = self.capture_logging("hook")
485
486 # Populate and verify some data we will
487 # later extract with the hook
488 expected = {"string": "rabbit",
489 "empty": "",
490 "emptylist": []}
491
492 exe = yield self.ua.get_invoker(
493 "db:42", "add", "mysql/0", self.mysql_relation,
494 client_id="client_id")
495
496 context = yield self.ua.get_context("client_id")
497 config = yield context.get_config()
498 config.update(expected)
499 yield config.write()
500
501 # invoke relation-get and verify the result
502 @defer.inlineCallbacks
503 def test_exit(variable, expected):
504 # --test can return a bad exit code from the POV of
505 # the invoker, this is fine, its just the way I'm using
506 # it to get at the value thats otherwise fine in the
507 # hook itself
508 try:
509 result = yield exe(self.create_hook("config-get",
510 "--test %s" % variable))
511 self.assertEqual(result, expected)
512 except errors.CharmInvocationError:
513 if expected == 1:
514 pass
515 data = hook_log.getvalue()
516 self.assertEqual(data, "")
517
518 yield test_exit("string", 0)
519 yield test_exit("emptylist", 1)
520 yield test_exit("emptystring", 1)
521
436522
437class RelationInvokerTestBase(InvokerTestBase, RelationTestBase):523class RelationInvokerTestBase(InvokerTestBase, RelationTestBase):
438524
@@ -684,6 +770,18 @@
684 self.assertEqual("mysql-0.example.com", data.strip())770 self.assertEqual("mysql-0.example.com", data.strip())
685771
686 @defer.inlineCallbacks772 @defer.inlineCallbacks
773 def test_unit_get_test_option(self):
774 """Private addresses can be tested for."""
775 hook_log = self.capture_logging("hook")
776 exe = yield self.ua.get_invoker(
777 "database:42", "add", "mysql/0", self.relation,
778 client_id="client_id")
779 result = yield exe(self.create_hook("unit-get", "--test private-address"))
780 self.assertEqual(result, 0)
781 data = hook_log.getvalue()
782 self.assertEqual("", data.strip())
783
784 @defer.inlineCallbacks
687 def test_spawn_cli_get_unit_unknown_public_address(self):785 def test_spawn_cli_get_unit_unknown_public_address(self):
688 """If for some hysterical raison, the public address hasn't been set.786 """If for some hysterical raison, the public address hasn't been set.
689787

Subscribers

People subscribed via source and target branches

to status/vote changes: