Merge lp:~jimbaker/pyjuju/charm-format-2 into lp:pyjuju

Proposed by Jim Baker
Status: Merged
Merged at revision: 544
Proposed branch: lp:~jimbaker/pyjuju/charm-format-2
Merge into: lp:pyjuju
Diff against target: 1785 lines (+1100/-163)
21 files modified
juju/charm/config.py (+6/-1)
juju/charm/metadata.py (+16/-6)
juju/charm/tests/repository/series/mysql-format-v2/config.yaml (+17/-0)
juju/charm/tests/repository/series/mysql-format-v2/metadata.yaml (+6/-0)
juju/charm/tests/repository/series/mysql-format-v2/revision (+1/-0)
juju/charm/tests/repository/series/mysql/config.yaml (+17/-0)
juju/charm/tests/repository/series/mysql/metadata.yaml (+1/-0)
juju/charm/tests/test_config.py (+3/-3)
juju/charm/tests/test_metadata.py (+24/-0)
juju/control/config_get.py (+34/-35)
juju/control/config_set.py (+5/-3)
juju/control/tests/test_config_set.py (+134/-0)
juju/hooks/cli.py (+6/-29)
juju/hooks/invoker.py (+10/-0)
juju/hooks/protocol.py (+32/-18)
juju/hooks/tests/test_cli.py (+98/-57)
juju/hooks/tests/test_communications.py (+9/-2)
juju/hooks/tests/test_invoker.py (+175/-2)
juju/lib/format.py (+144/-0)
juju/lib/tests/test_format.py (+330/-0)
juju/state/tests/test_relation.py (+32/-7)
To merge this branch: bzr merge lp:~jimbaker/pyjuju/charm-format-2
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+108831@code.launchpad.net

Description of the change

Adds format: 2 support to juju, while maintaining backwards compatibility for old charms.

Overview
========

This merge proposal adds support for a new key to a charm's
``metadata.yaml`` file, ``format`` such that charms can choose to use
a more consistent formatting (YAML) when working with relation and
configuration settings, without a backwards breaking change. Another
important part of this change is consistency: YAML is used throughout,
which works well with the existing YAML storage of relation/config
settings in ZK itself.

Example metadata
================

Here's an example metadata file for format: 2 charms:

name: wordpress
summary: "WordPress is a full featured web blogging tool"
maintainer: Clint Byrum <email address hidden>
description: |
 WordPress is a full featured web blogging tool:
 - Instant publishing (no rebuilding)
 - Comment pingback support with spam protection
 - Non-crufty URLs
 - Themable
 - Plugin support
requires:
  db:
    interface: mysql
provides:
  website:
    interface: http
format: 2

format: 1
=========

When using format: 1 (this format is implied when the format key is
undefined in ``metadata.yaml``), Juju for this charm maintains
existing format support for specifying configuration and relation
settings and for outputting relation settings when using the smart
format for ``relation-get``.

When using ASCII strings to define scalar values, this is not an issue
for working with relation settings. For config settings, because
everything is a string, it's not possible to actually set boolean
config settings (regardless of the capitalization):

$ juju set wordpress good=True
2012-06-05 14:44:35,893 INFO Connecting to environment...
2012-06-05 14:44:38,890 INFO Connected to environment.
Invalid value for good: 'True'
2012-06-05 14:44:39,108 ERROR Invalid value for good: 'True'

When a map is output (``relation-get -``), smart format continues to
use the ``__str__`` formatting, in this case of the internally used
Python dict. In this, we also see the impact of how parsing all values
as strings (in relation-set/set), then using JSON for encoding
transport converts all strings to Unicode. This results in output like
the following:

{u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
 u"configured": u"True",
 u"foo": u"bar"}

Per the discussion on the mailing list, this exposes Python
implementation details and needs to be fixed, especially in light of
the Go port.

format: 2
=========

When the format is set to 2, relation (``relation-set``) and config
(``juju set``) settings are parsed as YAML. When using
``relation-get`` with smart format, output is in YAML, using the same
YAML formatting options as ``juju get`` for consistency/human
readibility.

Output for v2 format is as follows for scalars, each on a separate line:

true # Input was true (or True or TRUE, YAML is especially tolerant of its input)
false
A string

Maps are output like so. A bash script likely could even readily
written to parse this output, without the use of a helper:

configured: true
foo: bar
public-address: ec2-1-2-3-4.compute-1.amazonaws.com

It is possible for charms of different charm format to be deployed in
an environment. Besides unit testing, this scenario has been
extensively tested when deployed.

New environment variable: CHARM_FORMAT
======================================

For relation-set/relation-get, the new environment variable
``CHARM_FORMAT``, as set for the relation hook context, controls
parsing and output. This can also be used in debug hooks to see the
impact of choosing one format versus the other.

Implementation notes
====================

Overall code changes are small, with most changes seen in tests. In
addition, tests were added to describe the current observed format: 1
behavior.

Two parallel code paths were implemented in impacted code. If it's
desired to modify the behavior of format: 2, this can be readily done
on that code path.

yaml.safe_load and yaml.safe_dump are used to ensure that Python types
do not leak and to avoid introducing a security hole. The use of YAML
also allows for consistent support of Unicode strings (not using ASCII
codepoints) as well as binary data (as introduced via the !!binary
tag). Unlike the format: 1 implementation, the consistent use of YAML
for encoding/decoding ensures that there is not an unexpected encoding
issue that's encountered, such as seen in bug #901495.

https://codereview.appspot.com/6308044/

To post a comment you must log in.
Revision history for this message
Jim Baker (jimbaker) wrote :
Download full text (5.4 KiB)

Reviewers: mp+108831_code.launchpad.net,

Message:
Please take a look.

Description:
Adds format: 2 support to juju, while maintaining backwards
compatibility for old charms.

Overview
========

This merge proposal adds support for a new key to a charm's
``metadata.yaml`` file, ``format`` such that charms can choose to use
a more consistent formatting (YAML) when working with relation and
configuration settings, without a backwards breaking change. Another
important part of this change is consistency: YAML is used throughout,
which works well with the existing YAML storage of relation/config
settings in ZK itself.

Example metadata
================

Here's an example metadata file for format: 2 charms:

name: wordpress
summary: "WordPress is a full featured web blogging tool"
maintainer: Clint Byrum <email address hidden>
description: |
  WordPress is a full featured web blogging tool:
  - Instant publishing (no rebuilding)
  - Comment pingback support with spam protection
  - Non-crufty URLs
  - Themable
  - Plugin support
requires:
   db:
     interface: mysql
provides:
   website:
     interface: http
format: 2

format: 1
=========

When using format: 1 (this format is implied when the format key is
undefined in ``metadata.yaml``), Juju for this charm maintains
existing format support for specifying configuration and relation
settings and for outputting relation settings when using the smart
format for ``relation-get``.

When using ASCII strings to define scalar values, this is not an issue
for working with relation settings. For config settings, because
everything is a string, it's not possible to actually set boolean
config settings (regardless of the capitalization):

$ juju set wordpress good=True
2012-06-05 14:44:35,893 INFO Connecting to environment...
2012-06-05 14:44:38,890 INFO Connected to environment.
Invalid value for good: 'True'
2012-06-05 14:44:39,108 ERROR Invalid value for good: 'True'

When a map is output (``relation-get -``), smart format continues to
use the ``__str__`` formatting, in this case of the internally used
Python dict. In this, we also see the impact of how parsing all values
as strings (in relation-set/set), then using JSON for encoding
transport converts all strings to Unicode. This results in output like
the following:

{u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
  u"configured": u"True",
  u"foo": u"bar"}

Per the discussion on the mailing list, this exposes Python
implementation details and needs to be fixed, especially in light of
the Go port.

format: 2
=========

When the format is set to 2, relation (``relation-set``) and config
(``juju set``) settings are parsed as YAML. When using
``relation-get`` with smart format, output is in YAML, using the same
YAML formatting options as ``juju get`` for consistency/human
readibility.

Output for v2 format is as follows for scalars, each on a separate line:

true # Input was true (or True or TRUE, YAML is especially tolerant
of its input)
false
A string

Maps are output like so. A bash script likely could even readily
written to parse this output, without the use of a helper:

configured: true
foo: bar
public-address: ec2-1-2-3-4.compute-1....

Read more...

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

I haven't reviewed the code, but the description of the change looks
good.

One detail that deserves some attention is the use of the CHARM_FORMAT
environment variable. If we have it, it should be named
JUJU_CHARM_FORMAT, similar to other variables we have. That said, do we
actually need this variable? This sounds like an implementation detail
that is not worth exposing. relation-get and relation-set both
communicate with the unit, and can readily discover the format based on
that.

https://codereview.appspot.com/6308044/

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

On further thinking, if the environment variable makes the change
simpler, we may as well just do it. Can we please make this purely an
implementation detail, though: call it _JUJU_CHARM_FORMAT, no docs. How
does that sound?

https://codereview.appspot.com/6308044/

Revision history for this message
Jim Baker (jimbaker) wrote :

On 2012/06/06 02:18:58, niemeyer wrote:
> On further thinking, if the environment variable makes the change
simpler, we
> may as well just do it. Can we please make this purely an
implementation detail,
> though: call it _JUJU_CHARM_FORMAT, no docs. How does that sound?

I like this suggestion: _JUJU_CHARM_FORMAT properly suggests this
environment variable is an implementation detail. The use of the env var
does simplify the implementation.

https://codereview.appspot.com/6308044/

Revision history for this message
William Reade (fwereade) wrote :
Download full text (5.7 KiB)

OK, this is a bit of a monster review... sorry :(. I have some
suggestions that could possibly simplify things quite a lot; please let
me know what you think.

https://codereview.appspot.com/6308044/diff/1/juju/charm/metadata.py
File juju/charm/metadata.py (right):

https://codereview.appspot.com/6308044/diff/1/juju/charm/metadata.py#newcode242
juju/charm/metadata.py:242: if self.format not in (1, 2):
(PYTHON_FORMAT, YAML_FORMAT)?

https://codereview.appspot.com/6308044/diff/1/juju/control/config_set.py
File juju/control/config_set.py (right):

https://codereview.appspot.com/6308044/diff/1/juju/control/config_set.py#newcode98
juju/control/config_set.py:98: os.environ["CHARM_FORMAT"] =
str(charm_format)
This feels like a strange way to go about modifying the behaviour of
parse_keyvalue_pairs below...

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode17
juju/hooks/cli.py:17: PYTHON_FORMAT, YAML_FORMAT = 1, 2
I feel like these should probably be somewhere else, if only because I
see late imports here and there. Suggestion: they, and
parse_keyvalue_pairs, should go somewhere else together; possibly
juju.charm?

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode243
juju/hooks/cli.py:243: print >>stream, serialized.rstrip("\n")
Appears to be duplicated in juju/charm/config.py; utility function?

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode247
juju/hooks/cli.py:247: """Return the format defined by $CHARM_FORMAT"""
Please make this function module-private; IMO this is the only place we
should be getting charm format from the environment (as opposed to...
from a charm ;)).

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode248
juju/hooks/cli.py:248: env_charm_format = os.environ.get("CHARM_FORMAT")
_JUJU_CHARM_FORMAT

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode275
juju/hooks/cli.py:275: charm_format = get_charm_format()
charm_format should be a parameter; it's easily accessible to both
clients.

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/invoker.py#newcode151
juju/hooks/invoker.py:151: self._charm_format = None
Add an accessor so the UnitAgentServer can get at this value.

https://codereview.appspot.com/6308044/diff/1/juju/hooks/invoker.py#newcode213
juju/hooks/invoker.py:213: CHARM_FORMAT=str(self._charm_format),
+1 on _JUJU_CHARM_FORMAT as suggested elsewhere. (note: it won't be
necessary in the go port.)

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/protocol.py#newcode101
juju/hooks/protocol.py:101: ("format", amp.Integer())]
This shouldn't need to be sent up here. The HookInvoker knows what the
value is, and the UnitAgentServer can access it via
self.factory.get_invoker(client_id).

https://codereview.appspot.com/6308044/diff/1/juju/hooks/protocol.py#newcode109
juju/hooks/protocol.py:109: ("format", a...

Read more...

Revision history for this message
William Reade (fwereade) wrote :

Another note: relation-ids and relation-list don't generate yaml; this
is odd, given that we favour yaml everywhere else; and while they should
not *default* to outputting yaml (ie we leave their format_smart methods
alone), we really *should* at least make the option available (by
implementing format_yaml on the base class).

https://codereview.appspot.com/6308044/

Revision history for this message
William Reade (fwereade) wrote :

Comments re: internal serialization format withdrawn; they're necessary
to preserve charm format 1 bugginess. Would you comment this explicitly
in somewhere protocol.py?

https://codereview.appspot.com/6308044/

Revision history for this message
Jim Baker (jimbaker) wrote :
Download full text (5.6 KiB)

Please take a look.

https://codereview.appspot.com/6308044/diff/1/juju/charm/metadata.py
File juju/charm/metadata.py (right):

https://codereview.appspot.com/6308044/diff/1/juju/charm/metadata.py#newcode242
juju/charm/metadata.py:242: if self.format not in (1, 2):
On 2012/06/06 13:34:40, fwereade wrote:
> (PYTHON_FORMAT, YAML_FORMAT)?

Done.

https://codereview.appspot.com/6308044/diff/1/juju/control/config_set.py
File juju/control/config_set.py (right):

https://codereview.appspot.com/6308044/diff/1/juju/control/config_set.py#newcode98
juju/control/config_set.py:98: os.environ["CHARM_FORMAT"] =
str(charm_format)
Agreed, just calls the refactored version of parse_keyvalue_pairs that
takes a charm format. Not certain what possessed me to write this code
this way, must have really want to avoid refactoring at that point :)

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode17
juju/hooks/cli.py:17: PYTHON_FORMAT, YAML_FORMAT = 1, 2
Moved to juju.lib.format. Putting this in juju.charm would seem to have
too much cross module dependency on what are otherwise independent
modules.

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode243
juju/hooks/cli.py:243: print >>stream, serialized.rstrip("\n")
Created juju.lib.format and moved this functionality there so it can be
reused as suggested.

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode247
juju/hooks/cli.py:247: """Return the format defined by $CHARM_FORMAT"""
That won't work, it's also needed by juju.hooks.protocol

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode248
juju/hooks/cli.py:248: env_charm_format = os.environ.get("CHARM_FORMAT")
On 2012/06/06 13:34:40, fwereade wrote:
> _JUJU_CHARM_FORMAT

Done.

https://codereview.appspot.com/6308044/diff/1/juju/hooks/cli.py#newcode275
juju/hooks/cli.py:275: charm_format = get_charm_format()
On 2012/06/06 13:34:40, fwereade wrote:
> charm_format should be a parameter; it's easily accessible to both
clients.

Done.

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/invoker.py#newcode151
juju/hooks/invoker.py:151: self._charm_format = None
On 2012/06/06 13:34:40, fwereade wrote:
> Add an accessor so the UnitAgentServer can get at this value.

Done.

https://codereview.appspot.com/6308044/diff/1/juju/hooks/invoker.py#newcode213
juju/hooks/invoker.py:213: CHARM_FORMAT=str(self._charm_format),
On 2012/06/06 13:34:40, fwereade wrote:
> +1 on _JUJU_CHARM_FORMAT as suggested elsewhere. (note: it won't be
necessary in
> the go port.)

Done.

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

https://codereview.appspot.com/6308044/diff/1/juju/hooks/protocol.py#newcode101
juju/hooks/protocol.py:101: ("format", amp.Integer())]
On 2012/06/06 13:34:40, fwereade wrote:
> This shouldn't need to be sent up here. The HookInvoker knows what the
value is,
> and the UnitAgentServer can access it via
self.factory.get_invoker(cl...

Read more...

lp:~jimbaker/pyjuju/charm-format-2 updated
559. By Jim Baker

Refactor def of PYTHON_FORMAT/YAML_FORMAT constants

560. By Jim Baker

Refactored parse_keyvalue_pairs

561. By Jim Baker

Use the invoker on the unit agent side of the protocol to retrieve charm_format

562. By Jim Baker

Refactored code out of juju.hooks.cli into juju.hooks.utils to avoid circular imports

563. By Jim Baker

Refactor to put standard formatting logic in juju.lib.format

564. By Jim Baker

Fix failing tests

565. By Jim Baker

Refactored format support for parsing/output to be shared across juju

566. By Jim Baker

Do not use str.format

567. By Jim Baker

PEP8/PyFlakes

568. By Jim Baker

Removed debug log stmt

Revision history for this message
Jim Baker (jimbaker) wrote :

Comments addressed in the latest version of the proposal.

https://codereview.appspot.com/6308044/

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

http://codereview.appspot.com/6308044/diff/10001/juju/charm/metadata.py
File juju/charm/metadata.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/charm/metadata.py#newcode239
juju/charm/metadata.py:239: if self.format not in (PYTHON_FORMAT,
YAML_FORMAT):
This seems to be conflating two different things, charm api versions and
there corresponding formats. ie. we could add version 3, with the
existing yaml format. there should be a mapping of format to version.
the charm isn't making a request for format, its making a version api
assertion.

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/protocol.py
File juju/hooks/protocol.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/protocol.py#newcode220
juju/hooks/protocol.py:220: if invoker.charm_format == PYTHON_FORMAT:
instead of conditional logic everywhere in this file, this process would
be simpler/cleaner dispatched to a formatter object from lib/format as
an instance property.

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/utils.py
File juju/hooks/utils.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/utils.py#newcode10
juju/hooks/utils.py:10: env_charm_format =
os.environ.get("_JUJU_CHARM_FORMAT")
this feels a bit icky, its an immutable property of a charm being passed
as a mutable parameter. the server side could return the format in its
response.

http://codereview.appspot.com/6308044/

Revision history for this message
Jim Baker (jimbaker) wrote :
lp:~jimbaker/pyjuju/charm-format-2 updated
569. By Jim Baker

Initial strategy refactoring

570. By Jim Baker

Do not expose YAMLFormat outside of juju.lib.format

571. By Jim Baker

Removed YAMLFormat, PythonFormat, etc in favor of using Format2, Forma1

572. By Jim Baker

More tests in juju.lib.format

573. By Jim Baker

More tests in juju.lib.format

574. By Jim Baker

More tests

575. By Jim Baker

Yet more tests/docstrings

576. By Jim Baker

PyFlakes

577. By Jim Baker

Merged trunk

Revision history for this message
Jim Baker (jimbaker) wrote :

Please take a look at the latest proposed version of this branch, this
should solve the issues that were raised.

http://codereview.appspot.com/6308044/diff/10001/juju/charm/metadata.py
File juju/charm/metadata.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/charm/metadata.py#newcode239
juju/charm/metadata.py:239: if self.format not in (PYTHON_FORMAT,
YAML_FORMAT):
On 2012/06/12 21:18:08, hazmat wrote:
> This seems to be conflating two different things, charm api versions
and there
> corresponding formats. ie. we could add version 3, with the existing
yaml
> format. there should be a mapping of format to version. the charm
isn't making a
> request for format, its making a version api assertion.

I changed this so there's a definite distinction, through the use of
Format1 and Format2 classes (no more PYTHON_FORMAT, YAML_FORMAT). Assume
at some point we have a format: 3 and it's supported by the Python code,
then we could readily create a Format3 class that either derives from
Format2, or from BaseFormat, or what makes sense. So this separates the
charm version information (and any assertions) from the implementation.

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/protocol.py
File juju/hooks/protocol.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/protocol.py#newcode220
juju/hooks/protocol.py:220: if invoker.charm_format == PYTHON_FORMAT:
The code has been changed such that one can get an appropriate formatter
(Format1, Format2 for now) and do the appropriate action, without the
conditional logic being replicated throughout as you've observed.

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/utils.py
File juju/hooks/utils.py (right):

http://codereview.appspot.com/6308044/diff/10001/juju/hooks/utils.py#newcode10
juju/hooks/utils.py:10: env_charm_format =
os.environ.get("_JUJU_CHARM_FORMAT")
I have changed this usage where feasible to what you describe, however,
in cases like relation_set, it's necessary to do the parsing on the
client side before calling the server side of the implementation. In
this case, it's still necessary to define _JUJU_CHARM_FORMAT. However,
this lookup is now more isolated, because it's been moved to
juju.lib.format.

http://codereview.appspot.com/6308044/

Revision history for this message
William Reade (fwereade) wrote :

Damn, I'm sorry, I had an unpublished LGTM sitting here for days :(((.

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

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/tests/test_invoker.py#newcode511
juju/hooks/tests/test_invoker.py:511: @defer.inlineCallbacks
Heh, nice catches :).

https://codereview.appspot.com/6308044/diff/11004/juju/lib/format.py
File juju/lib/format.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/lib/format.py#newcode144
juju/lib/format.py:144: os.environ.get("_JUJU_CHARM_FORMAT", "1")))
Thanks, all this is much clearer.

https://codereview.appspot.com/6308044/

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

i'd still like to get rid of the env var, but i can live with it. just
changing the format2 to yamlformat, format1 to pythonformat and it lgtm.

https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py
File juju/charm/config.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py#newcode8
juju/charm/config.py:8: from juju.lib.schema import (SchemaError,
KeyDict, Dict, String,
please call this yamlformat, format2 is not what the output format is.

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py
File juju/charm/metadata.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py#newcode143
juju/charm/metadata.py:143: """Optional charm format, defaults to 1"""
minor, a pointer to a reference on what these entail would be useful
here.

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py
File juju/hooks/protocol.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py#newcode405
juju/hooks/protocol.py:405: formatter = get_charm_formatter_from_env()
you don't need this here, this is an internal transport detail, you can
just pick a format, and that removes the need for env formatter
selection. the server knowing the charm format can specify it in its
response.

https://codereview.appspot.com/6308044/

Revision history for this message
Jim Baker (jimbaker) wrote :

On 2012/06/19 20:01:09, hazmat wrote:
> i'd still like to get rid of the env var, but i can live with it. just
changing
> the format2 to yamlformat, format1 to pythonformat and it lgtm.

Please note that format: 1 implies both Python str encoding and the use
of JSON dump/load which brings in Unicode promotion of strings (part of
the underlying bugginess). So that's why I chose to use Format1 here.
format:2 cleans this up by using YAML throughout, matching the
serialization into ZK for relation settings.

However, it's certainly easy enough to change to PythonFormat and
YAMLFormat, just wanted to explain the rationale here.

  https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py
> File juju/charm/config.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py#newcode8
> juju/charm/config.py:8: from juju.lib.schema import (SchemaError,
KeyDict, Dict,
> String,
> please call this yamlformat, format2 is not what the output format is.

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py
> File juju/charm/metadata.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py#newcode143
> juju/charm/metadata.py:143: """Optional charm format, defaults to 1"""
> minor, a pointer to a reference on what these entail would be useful
here.

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py
> File juju/hooks/protocol.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py#newcode405
> juju/hooks/protocol.py:405: formatter = get_charm_formatter_from_env()
> you don't need this here, this is an internal transport detail, you
can just
> pick a format, and that removes the need for env formatter selection.
the server
> knowing the charm format can specify it in its response.

https://codereview.appspot.com/6308044/

Revision history for this message
Jim Baker (jimbaker) wrote :

Responded to Kapil's comments.

https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py
File juju/charm/config.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/config.py#newcode8
juju/charm/config.py:8: from juju.lib.schema import (SchemaError,
KeyDict, Dict, String,
On 2012/06/19 20:01:09, hazmat wrote:
> please call this yamlformat, format2 is not what the output format is.

Ack.

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py
File juju/charm/metadata.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/charm/metadata.py#newcode143
juju/charm/metadata.py:143: """Optional charm format, defaults to 1"""
On 2012/06/19 20:01:09, hazmat wrote:
> minor, a pointer to a reference on what these entail would be useful
here.

Ack.

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py
File juju/hooks/protocol.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/protocol.py#newcode405
juju/hooks/protocol.py:405: formatter = get_charm_formatter_from_env()
On 2012/06/19 20:01:09, hazmat wrote:
> you don't need this here, this is an internal transport detail, you
can just
> pick a format, and that removes the need for env formatter selection.
the server
> knowing the charm format can specify it in its response.

Actually I tried this early on, and found that the proposed strategy
doesn't work. The problem is that format: 1 is sensitive to the fact
that it uses a JSON encoding/decoding step, and this is tested, among
other places, by juju.hooks.tests.test_invoker.TestCharmFormatV1
(changed to TestPythonFormat). William raised the same issue in his
report, and we discussed on IRC earlier. See
https://codereview.appspot.com/6308044/diff/1/juju/hooks/protocol.py#newcode219
and https://codereview.appspot.com/6308044/#msg7. In particular, this
would make the unicode promotion from str go away. That's a nice thing,
but bugginess is what we are trying to preserve.

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

https://codereview.appspot.com/6308044/diff/11004/juju/hooks/tests/test_invoker.py#newcode511
juju/hooks/tests/test_invoker.py:511: @defer.inlineCallbacks
On 2012/06/18 22:46:41, fwereade wrote:
> Heh, nice catches :).
Thanks!

https://codereview.appspot.com/6308044/diff/11004/juju/lib/format.py
File juju/lib/format.py (right):

https://codereview.appspot.com/6308044/diff/11004/juju/lib/format.py#newcode144
juju/lib/format.py:144: os.environ.get("_JUJU_CHARM_FORMAT", "1")))
On 2012/06/18 22:46:41, fwereade wrote:
> Thanks, all this is much clearer.

Cool!

https://codereview.appspot.com/6308044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/charm/config.py'
--- juju/charm/config.py 2012-03-07 21:19:46 +0000
+++ juju/charm/config.py 2012-06-15 02:00:25 +0000
@@ -4,6 +4,7 @@
44
5import yaml5import yaml
66
7from juju.lib.format import Format2
7from juju.lib.schema import (SchemaError, KeyDict, Dict, String,8from juju.lib.schema import (SchemaError, KeyDict, Dict, String,
8 Constant, OneOf, Int, Float)9 Constant, OneOf, Int, Float)
9from juju.charm.errors import (10from juju.charm.errors import (
@@ -138,8 +139,12 @@
138 value, valid = validator(value, self._data[name])139 value, valid = validator(value, self._data[name])
139140
140 if not valid:141 if not valid:
142 # Return value such that it roundtrips; this allows us to
143 # report back the boolean false instead of the Python
144 # output format, False
141 raise ServiceConfigValueError(145 raise ServiceConfigValueError(
142 "Invalid value for %s: %r" % (name, value))146 "Invalid value for %s: %s" % (
147 name, Format2().format(value)))
143 return value148 return value
144149
145 def get_defaults(self):150 def get_defaults(self):
146151
=== modified file 'juju/charm/metadata.py'
--- juju/charm/metadata.py 2012-02-28 01:47:53 +0000
+++ juju/charm/metadata.py 2012-06-15 02:00:25 +0000
@@ -5,20 +5,17 @@
55
6from juju.charm.errors import MetaDataError6from juju.charm.errors import MetaDataError
7from juju.errors import FileNotFound7from juju.errors import FileNotFound
8from juju.lib.format import is_valid_charm_format
8from juju.lib.schema import (9from juju.lib.schema import (
9 SchemaError, Bool, Constant, Dict, Int,10 SchemaError, Bool, Constant, Dict, Int,
10 KeyDict, OneOf, UnicodeOrString)11 KeyDict, OneOf, UnicodeOrString)
1112
1213
13log = logging.getLogger("juju.charm")14log = logging.getLogger("juju.charm")
14
15
16UTF8_SCHEMA = UnicodeOrString("utf-8")15UTF8_SCHEMA = UnicodeOrString("utf-8")
17
18SCOPE_GLOBAL = "global"16SCOPE_GLOBAL = "global"
19SCOPE_CONTAINER = "container"17SCOPE_CONTAINER = "container"
2018
21
22INTERFACE_SCHEMA = KeyDict({19INTERFACE_SCHEMA = KeyDict({
23 "interface": UTF8_SCHEMA,20 "interface": UTF8_SCHEMA,
24 "limit": OneOf(Constant(None), Int()),21 "limit": OneOf(Constant(None), Int()),
@@ -92,11 +89,14 @@
92 "revision": Int(),89 "revision": Int(),
93 "summary": UTF8_SCHEMA,90 "summary": UTF8_SCHEMA,
94 "description": UTF8_SCHEMA,91 "description": UTF8_SCHEMA,
92 "format": Int(),
95 "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)),93 "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)),
96 "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)),94 "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)),
97 "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)),95 "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)),
98 "subordinate": Bool(),96 "subordinate": Bool(),
99 }, optional=set(["provides", "requires", "peers", "revision", "subordinate"]))97 }, optional=set(
98 ["format", "provides", "requires", "peers", "revision",
99 "subordinate"]))
100100
101101
102class MetaData(object):102class MetaData(object):
@@ -139,6 +139,11 @@
139 return self._data.get("description")139 return self._data.get("description")
140140
141 @property141 @property
142 def format(self):
143 """Optional charm format, defaults to 1"""
144 return self._data.get("format", 1)
145
146 @property
142 def provides(self):147 def provides(self):
143 """The charm provides relations."""148 """The charm provides relations."""
144 return self._data.get("provides")149 return self._data.get("provides")
@@ -211,7 +216,8 @@
211 for rel in self.provides:216 for rel in self.provides:
212 if rel.startswith("juju-"):217 if rel.startswith("juju-"):
213 raise MetaDataError(218 raise MetaDataError(
214 "Charm %s attempting to provide relation in implicit relation namespace: %s" %219 "Charm %s attempting to provide relation in "
220 "implicit relation namespace: %s" %
215 (self.name, rel))221 (self.name, rel))
216 interface = self.provides[rel]["interface"]222 interface = self.provides[rel]["interface"]
217 if interface.startswith("juju-"):223 if interface.startswith("juju-"):
@@ -230,6 +236,10 @@
230 "%s labeled subordinate but lacking scope:container `requires` relation" %236 "%s labeled subordinate but lacking scope:container `requires` relation" %
231 path)237 path)
232238
239 if not is_valid_charm_format(self.format):
240 raise MetaDataError("Charm %s uses an unknown format: %s" % (
241 self.name, self.format))
242
233 def parse_serialization_data(self, serialization_data, path=None):243 def parse_serialization_data(self, serialization_data, path=None):
234 """Parse the unprocessed serialization data and load in this instance.244 """Parse the unprocessed serialization data and load in this instance.
235245
236246
=== added directory 'juju/charm/tests/repository/series/mysql-format-v2'
=== added file 'juju/charm/tests/repository/series/mysql-format-v2/config.yaml'
--- juju/charm/tests/repository/series/mysql-format-v2/config.yaml 1970-01-01 00:00:00 +0000
+++ juju/charm/tests/repository/series/mysql-format-v2/config.yaml 2012-06-15 02:00:25 +0000
@@ -0,0 +1,17 @@
1options:
2 query-cache-size:
3 default: -1
4 type: int
5 description: Override the computed version from dataset-size. Still works if query-cache-type is "OFF" since sessions can override the cache type setting on their own.
6 awesome:
7 default: false
8 type: boolean
9 description: Set true to make this database engine truly awesome
10 tuning-level:
11 default: safest
12 type: string
13 description: Valid values are 'safest', 'fast', and 'unsafe'. If set to safest, all settings are tuned to have maximum safety at the cost of performance. Fast will turn off most controls, but may lose data on crashes. unsafe will turn off all protections.
14 monkey-madness:
15 default: 0.5
16 type: float
17 description: The amount of randomness to be desired in any data that is returned, from 0 (sane) to 1 (monkeys running the asylum).
018
=== added file 'juju/charm/tests/repository/series/mysql-format-v2/metadata.yaml'
--- juju/charm/tests/repository/series/mysql-format-v2/metadata.yaml 1970-01-01 00:00:00 +0000
+++ juju/charm/tests/repository/series/mysql-format-v2/metadata.yaml 2012-06-15 02:00:25 +0000
@@ -0,0 +1,6 @@
1name: mysql-format-v2
2summary: "Database engine"
3description: "A pretty popular database"
4provides:
5 server: mysql
6format: 2
07
=== added file 'juju/charm/tests/repository/series/mysql-format-v2/revision'
--- juju/charm/tests/repository/series/mysql-format-v2/revision 1970-01-01 00:00:00 +0000
+++ juju/charm/tests/repository/series/mysql-format-v2/revision 2012-06-15 02:00:25 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added file 'juju/charm/tests/repository/series/mysql/config.yaml'
--- juju/charm/tests/repository/series/mysql/config.yaml 1970-01-01 00:00:00 +0000
+++ juju/charm/tests/repository/series/mysql/config.yaml 2012-06-15 02:00:25 +0000
@@ -0,0 +1,17 @@
1options:
2 query-cache-size:
3 default: -1
4 type: int
5 description: Override the computed version from dataset-size. Still works if query-cache-type is "OFF" since sessions can override the cache type setting on their own.
6 awesome:
7 default: false
8 type: boolean
9 description: Set true to make this database engine truly awesome
10 tuning-level:
11 default: safest
12 type: string
13 description: Valid values are 'safest', 'fast', and 'unsafe'. If set to safest, all settings are tuned to have maximum safety at the cost of performance. Fast will turn off most controls, but may lose data on crashes. unsafe will turn off all protections.
14 monkey-madness:
15 default: 0.5
16 type: float
17 description: The amount of randomness to be desired in any data that is returned, from 0 (sane) to 1 (monkeys running the asylum).
018
=== modified file 'juju/charm/tests/repository/series/mysql/metadata.yaml'
--- juju/charm/tests/repository/series/mysql/metadata.yaml 2011-10-04 19:13:12 +0000
+++ juju/charm/tests/repository/series/mysql/metadata.yaml 2012-06-15 02:00:25 +0000
@@ -3,3 +3,4 @@
3description: "A pretty popular database"3description: "A pretty popular database"
4provides:4provides:
5 server: mysql5 server: mysql
6format: 1
67
=== modified file 'juju/charm/tests/test_config.py'
--- juju/charm/tests/test_config.py 2012-03-07 21:19:46 +0000
+++ juju/charm/tests/test_config.py 2012-06-15 02:00:25 +0000
@@ -89,7 +89,7 @@
89 "type": "string",89 "type": "string",
90 "default": True}}}))90 "default": True}}}))
91 self.assertEqual(91 self.assertEqual(
92 str(e), "Invalid value for foobar: True")92 str(e), "Invalid value for foobar: true")
9393
94 def test_as_dict(self):94 def test_as_dict(self):
95 # load valid data95 # load valid data
@@ -160,7 +160,7 @@
160160
161 error = self.assertRaises(ServiceConfigValueError,161 error = self.assertRaises(ServiceConfigValueError,
162 self.config.validate, {"title": True})162 self.config.validate, {"title": True})
163 self.assertEquals(str(error), "Invalid value for title: True")163 self.assertEquals(str(error), "Invalid value for title: true")
164164
165 data = self.config.validate({"title": u"Good"})165 data = self.config.validate({"title": u"Good"})
166 self.assertEqual(data, {"title": u"Good"})166 self.assertEqual(data, {"title": u"Good"})
@@ -184,7 +184,7 @@
184184
185 error = self.assertRaises(ServiceConfigValueError,185 error = self.assertRaises(ServiceConfigValueError,
186 self.config.validate, {"skill-level": "NaN"})186 self.config.validate, {"skill-level": "NaN"})
187 self.assertEquals(str(error), "Invalid value for skill-level: 'NaN'")187 self.assertEquals(str(error), "Invalid value for skill-level: NaN")
188188
189 data = self.config.validate({"skill-level": "9001"})189 data = self.config.validate({"skill-level": "9001"})
190 # its over 9000!190 # its over 9000!
191191
=== modified file 'juju/charm/tests/test_metadata.py'
--- juju/charm/tests/test_metadata.py 2012-02-28 01:47:53 +0000
+++ juju/charm/tests/test_metadata.py 2012-06-15 02:00:25 +0000
@@ -60,6 +60,7 @@
60 self.assertEquals(self.metadata.summary, None)60 self.assertEquals(self.metadata.summary, None)
61 self.assertEquals(self.metadata.description, None)61 self.assertEquals(self.metadata.description, None)
62 self.assertEquals(self.metadata.is_subordinate, False)62 self.assertEquals(self.metadata.is_subordinate, False)
63 self.assertEquals(self.metadata.format, 1)
6364
64 def test_parse_and_check_basic_info(self):65 def test_parse_and_check_basic_info(self):
65 """66 """
@@ -238,6 +239,29 @@
238 "Charm dummy attempting to provide interface in implicit namespace: juju-magic (relation: foo-rel)",239 "Charm dummy attempting to provide interface in implicit namespace: juju-magic (relation: foo-rel)",
239 str(error))240 str(error))
240241
242 def test_format(self):
243 # Defaults to 1
244 self.metadata.parse(self.sample)
245 self.assertEquals(self.metadata.format, 1)
246
247 # Explicitly set to 1
248 with self.change_sample() as data:
249 data["format"] = 1
250 self.metadata.parse(self.sample)
251 self.assertEquals(self.metadata.format, 1)
252
253 # Explicitly set to 2
254 with self.change_sample() as data:
255 data["format"] = 2
256 self.metadata.parse(self.sample)
257 self.assertEquals(self.metadata.format, 2)
258
259 # Explicitly set to 3; however this is an unknown format for Juju
260 with self.change_sample() as data:
261 data["format"] = 3
262 error = self.assertRaises(MetaDataError, self.metadata.parse, self.sample)
263 self.assertIn("Charm dummy uses an unknown format: 3", str(error))
264
241265
242class ParseTest(TestCase):266class ParseTest(TestCase):
243 """Test the parsing of some well-known sample files"""267 """Test the parsing of some well-known sample files"""
244268
=== modified file 'juju/control/config_get.py'
--- juju/control/config_get.py 2012-01-30 22:20:37 +0000
+++ juju/control/config_get.py 2012-06-15 02:00:25 +0000
@@ -1,9 +1,9 @@
1import argparse1import argparse
2import yaml
32
4from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
54
6from juju.control.utils import get_environment5from juju.control.utils import get_environment
6from juju.lib.format import Format2
7from juju.state.service import ServiceStateManager7from juju.state.service import ServiceStateManager
88
99
@@ -59,38 +59,37 @@
59 """59 """
60 provider = environment.get_machine_provider()60 provider = environment.get_machine_provider()
61 client = yield provider.connect()61 client = yield provider.connect()
6262 try:
63 # Get the service63 # Get the service
64 service_manager = ServiceStateManager(client)64 service_manager = ServiceStateManager(client)
65 service = yield service_manager.get_service_state(service_name)65 service = yield service_manager.get_service_state(service_name)
6666
67 # Retrieve schema67 # Retrieve schema
68 charm = yield service.get_charm_state()68 charm = yield service.get_charm_state()
69 schema = yield charm.get_config()69 schema = yield charm.get_config()
70 schema_dict = schema.as_dict()70 schema_dict = schema.as_dict()
71 display_dict = {"service": service.service_name,71 display_dict = {"service": service.service_name,
72 "charm": (yield service.get_charm_id()),72 "charm": (yield service.get_charm_id()),
73 "settings": schema_dict}73 "settings": schema_dict}
7474
75 # Get current settings75 # Get current settings
76 settings = yield service.get_config()76 settings = yield service.get_config()
77 settings = dict(settings.items())77 settings = dict(settings.items())
7878
79 # Merge current settings into schema/display dict79 # Merge current settings into schema/display dict
80 for k, v in schema_dict.items():80 for k, v in schema_dict.items():
81 # Display defaults for unset values.81 # Display defaults for unset values.
82 if k in settings:82 if k in settings:
83 v['value'] = settings[k]83 v['value'] = settings[k]
84 else:
85 v['value'] = "-Not set-"
86
87 if 'default' in v:
88 if v['default'] == settings[k]:
89 v['default'] = True
90 else:84 else:
91 del v['default']85 v['value'] = "-Not set-"
9286
93 print yaml.safe_dump(87 if 'default' in v:
94 display_dict,88 if v['default'] == settings[k]:
95 indent=4, default_flow_style=False, width=80, allow_unicode=True)89 v['default'] = True
96 client.close()90 else:
91 del v['default']
92
93 print Format2().format(display_dict)
94 finally:
95 yield client.close()
9796
=== modified file 'juju/control/config_set.py'
--- juju/control/config_set.py 2012-04-04 13:00:13 +0000
+++ juju/control/config_set.py 2012-06-15 02:00:25 +0000
@@ -6,7 +6,7 @@
66
7from juju.charm.errors import ServiceConfigValueError7from juju.charm.errors import ServiceConfigValueError
8from juju.control.utils import get_environment8from juju.control.utils import get_environment
9from juju.hooks.cli import parse_keyvalue_pairs9from juju.lib.format import get_charm_formatter
10from juju.state.service import ServiceStateManager10from juju.state.service import ServiceStateManager
1111
1212
@@ -62,7 +62,7 @@
6262
63 yaml_data = options.config.read()63 yaml_data = options.config.read()
64 try:64 try:
65 data = yaml.load(yaml_data)65 data = yaml.safe_load(yaml_data)
66 except yaml.YAMLError:66 except yaml.YAMLError:
67 raise ServiceConfigValueError(67 raise ServiceConfigValueError(
68 "Config file %r not valid YAML" % options.config.name)68 "Config file %r not valid YAML" % options.config.name)
@@ -93,6 +93,8 @@
93 service_manager = ServiceStateManager(client)93 service_manager = ServiceStateManager(client)
94 service = yield service_manager.get_service_state(service_name)94 service = yield service_manager.get_service_state(service_name)
95 charm = yield service.get_charm_state()95 charm = yield service.get_charm_state()
96 charm_format = (yield charm.get_metadata()).format
97 formatter = get_charm_formatter(charm_format)
9698
97 # Use the charm's ConfigOptions instance to validate the99 # Use the charm's ConfigOptions instance to validate the
98 # arguments to config_set. Invalid options passed to this method100 # arguments to config_set. Invalid options passed to this method
@@ -100,7 +102,7 @@
100 if isinstance(service_options, dict):102 if isinstance(service_options, dict):
101 options = service_options103 options = service_options
102 else:104 else:
103 options = parse_keyvalue_pairs(service_options)105 options = formatter.parse_keyvalue_pairs(service_options)
104106
105 config = yield charm.get_config()107 config = yield charm.get_config()
106 # ignore the output of validate, we run it so it might throw an exception108 # ignore the output of validate, we run it so it might throw an exception
107109
=== modified file 'juju/control/tests/test_config_set.py'
--- juju/control/tests/test_config_set.py 2012-03-27 22:46:44 +0000
+++ juju/control/tests/test_config_set.py 2012-06-15 02:00:25 +0000
@@ -182,3 +182,137 @@
182 yield state.read()182 yield state.read()
183 self.assertEqual(state, {"foo": "new foo",183 self.assertEqual(state, {"foo": "new foo",
184 "bar": "new bar"})184 "bar": "new bar"})
185
186 @inlineCallbacks
187 def test_boolean_option_invalid_format_v1(self):
188 """Verify not possible to set a boolean option with format v1"""
189 self.service_state = yield self.add_service_from_charm("mysql")
190 finished = self.setup_cli_reactor()
191 self.setup_exit(0)
192 self.mocker.replay()
193 main(["set", "mysql","awesome=true"])
194 yield finished
195 self.assertEqual(
196 self.stderr.getvalue(),
197 "Invalid value for awesome: 'true'\n")
198 state = yield self.service_state.get_config()
199 self.assertEqual(
200 state,
201 {"awesome": False, "monkey-madness": 0.5,
202 "query-cache-size": -1, "tuning-level": "safest"})
203
204 @inlineCallbacks
205 def test_int_option_coerced_format_v1(self):
206 """Verify int feasible, but coerced internally to str in format v1"""
207 self.service_state = yield self.add_service_from_charm("mysql")
208 finished = self.setup_cli_reactor()
209 self.setup_exit(0)
210 self.mocker.replay()
211 main(["set", "mysql","query-cache-size=10"])
212 yield finished
213 self.assertEqual(self.stderr.getvalue(), "")
214 state = yield self.service_state.get_config()
215 self.assertEqual(
216 state,
217 {"awesome": False, "monkey-madness": 0.5,
218 "query-cache-size": "10", "tuning-level": "safest"})
219
220 @inlineCallbacks
221 def test_float_option_invalid_format_v1(self):
222 """Verify not possible to set a float option with format v1"""
223 self.service_state = yield self.add_service_from_charm("mysql")
224 finished = self.setup_cli_reactor()
225 self.setup_exit(0)
226 self.mocker.replay()
227 main(["set", "mysql","monkey-madness=0.99999999"])
228 yield finished
229 self.assertEqual(
230 self.stderr.getvalue(),
231 "Invalid value for monkey-madness: '0.99999999'\n")
232 state = yield self.service_state.get_config()
233 self.assertEqual(
234 state,
235 {"awesome": False, "monkey-madness": 0.5,
236 "query-cache-size": -1, "tuning-level": "safest"})
237
238 @inlineCallbacks
239 def test_valid_options_format_v2(self):
240 """Verify that config settings can be properly parsed and applied"""
241 self.service_state = yield self.add_service_from_charm(
242 "mysql-format-v2")
243 finished = self.setup_cli_reactor()
244 self.setup_exit(0)
245 self.mocker.replay()
246 main(["set",
247 "mysql-format-v2",
248 "query-cache-size=100",
249 "awesome=true",
250 "tuning-level=unsafe",
251 "monkey-madness=0.97"])
252 yield finished
253 self.assertEqual(self.stderr.getvalue(), "")
254 state = yield self.service_state.get_config()
255 self.assertEqual(
256 state,
257 {"awesome": True, "monkey-madness": 0.97,
258 "query-cache-size": 100, "tuning-level": "unsafe"})
259
260 @inlineCallbacks
261 def test_invalid_float_option_format_v2(self):
262 """Verify that config settings reject invalid floats"""
263 self.service_state = yield self.add_service_from_charm(
264 "mysql-format-v2")
265 finished = self.setup_cli_reactor()
266 self.setup_exit(0)
267 self.mocker.replay()
268 main(["set", "mysql-format-v2",
269 "monkey-madness='barrels of monkeys'"])
270 yield finished
271 self.assertEqual(
272 self.stderr.getvalue(),
273 "Invalid value for monkey-madness: barrels of monkeys\n")
274 state = yield self.service_state.get_config()
275 self.assertEqual(
276 state,
277 {"awesome": False, "monkey-madness": 0.5,
278 "query-cache-size": -1, "tuning-level": "safest"})
279
280 @inlineCallbacks
281 def test_invalid_int_option_format_v2(self):
282 """Verify that config settings reject invalid ints"""
283 self.service_state = yield self.add_service_from_charm(
284 "mysql-format-v2")
285 finished = self.setup_cli_reactor()
286 self.setup_exit(0)
287 self.mocker.replay()
288 main(["set", "mysql-format-v2", "query-cache-size=big"])
289 yield finished
290 self.assertEqual(
291 self.stderr.getvalue(),
292 "Invalid value for query-cache-size: big\n")
293 state = yield self.service_state.get_config()
294 self.assertEqual(
295 state,
296 {"awesome": False, "monkey-madness": 0.5,
297 "query-cache-size": -1, "tuning-level": "safest"})
298
299 @inlineCallbacks
300 def test_invalid_string_option_format_v2(self):
301 """Verify that config settings reject invalid string"""
302 self.service_state = yield self.add_service_from_charm(
303 "mysql-format-v2")
304 finished = self.setup_cli_reactor()
305 self.setup_exit(0)
306 self.mocker.replay()
307 # YAML does a fair amount of coercion... note that it needs to
308 # be quoted if it looks like a boolean/int/string
309 main(["set", "mysql-format-v2", "tuning-level=FALSE"])
310 yield finished
311 self.assertEqual(
312 self.stderr.getvalue(),
313 "Invalid value for tuning-level: false\n")
314 state = yield self.service_state.get_config()
315 self.assertEqual(
316 state,
317 {"awesome": False, "monkey-madness": 0.5,
318 "query-cache-size": -1, "tuning-level": "safest"})
185319
=== modified file 'juju/hooks/cli.py'
--- juju/hooks/cli.py 2012-05-02 00:49:10 +0000
+++ juju/hooks/cli.py 2012-06-15 02:00:25 +0000
@@ -8,8 +8,9 @@
8from twisted.internet import defer8from twisted.internet import defer
9from twisted.internet import protocol9from twisted.internet import protocol
1010
11from juju.errors import JujuError
12from juju.hooks.protocol import UnitAgentClient11from juju.hooks.protocol import UnitAgentClient
12from juju.lib.format import get_charm_formatter_from_env
13
1314
14_marker = object()15_marker = object()
1516
@@ -37,7 +38,6 @@
3738
38 `require_cid` -- Does the command require the specification of a39 `require_cid` -- Does the command require the specification of a
39 client_id. (default: True)40 client_id. (default: True)
40
41 """41 """
4242
43 default_mode = "wb"43 default_mode = "wb"
@@ -153,7 +153,8 @@
153 stream=self.options.log_file)153 stream=self.options.log_file)
154154
155 def parse_kvpairs(self, options):155 def parse_kvpairs(self, options):
156 data = parse_keyvalue_pairs(options)156 formatter = get_charm_formatter_from_env()
157 data = formatter.parse_keyvalue_pairs(options)
157 # cache158 # cache
158 self.options.keyvalue_pairs = data159 self.options.keyvalue_pairs = data
159 return data160 return data
@@ -226,7 +227,8 @@
226227
227 def format_smart(self, result, stream):228 def format_smart(self, result, stream):
228 if result is not None:229 if result is not None:
229 print >>stream, str(result)230 charm_formatter = get_charm_formatter_from_env()
231 print >>stream, charm_formatter.format(result)
230232
231233
232def parse_log_level(level):234def parse_log_level(level):
@@ -244,31 +246,6 @@
244 return level246 return level
245247
246248
247def parse_keyvalue_pairs(options):
248 data = {}
249 for kv in options:
250 if "=" not in kv:
251 raise JujuError(
252 "Expected `option=value`. Found `%s`" % kv)
253
254 k, v = kv.split("=", 1)
255 if v.startswith("@"):
256 # handle fileinput per spec
257 # XXX: todo -- sanitize input
258 filename = v[1:]
259 try:
260 with open(filename, "r") as f:
261 v = f.read()
262 except IOError:
263 raise JujuError(
264 "No such file or directory: %s (argument:%s)" % (
265 filename,
266 k))
267 data[k] = v
268
269 return data
270
271
272def parse_port_protocol(port_protocol_string):249def parse_port_protocol(port_protocol_string):
273 """Returns (`port`, `protocol`) by converting `port_protocol_string`.250 """Returns (`port`, `protocol`) by converting `port_protocol_string`.
274251
275252
=== modified file 'juju/hooks/invoker.py'
--- juju/hooks/invoker.py 2012-05-01 00:26:25 +0000
+++ juju/hooks/invoker.py 2012-06-15 02:00:25 +0000
@@ -148,6 +148,7 @@
148 self._socket_path = socket_path148 self._socket_path = socket_path
149 self._unit_path = unit_path149 self._unit_path = unit_path
150 self._log = logger150 self._log = logger
151 self._charm_format = None
151152
152 # The twisted.internet.process.Process instance.153 # The twisted.internet.process.Process instance.
153 self._process = None154 self._process = None
@@ -187,6 +188,14 @@
187 display_parent_relation_ident,188 display_parent_relation_ident,
188 sorted(relation_idents)))189 sorted(relation_idents)))
189190
191 service = yield self._context.get_local_service()
192 charm = yield service.get_charm_state()
193 self._charm_format = (yield charm.get_metadata()).format
194
195 @property
196 def charm_format(self):
197 return self._charm_format
198
190 @property199 @property
191 def ended(self):200 def ended(self):
192 return self._ended201 return self._ended
@@ -205,6 +214,7 @@
205 base = dict(JUJU_AGENT_SOCKET=self._socket_path,214 base = dict(JUJU_AGENT_SOCKET=self._socket_path,
206 JUJU_CLIENT_ID=self._client_id,215 JUJU_CLIENT_ID=self._client_id,
207 CHARM_DIR=os.path.join(self._unit_path, "charm"),216 CHARM_DIR=os.path.join(self._unit_path, "charm"),
217 _JUJU_CHARM_FORMAT=str(self.charm_format),
208 JUJU_UNIT_NAME=os.environ["JUJU_UNIT_NAME"],218 JUJU_UNIT_NAME=os.environ["JUJU_UNIT_NAME"],
209 DEBIAN_FRONTEND="noninteractive",219 DEBIAN_FRONTEND="noninteractive",
210 APT_LISTCHANGES_FRONTEND="none",220 APT_LISTCHANGES_FRONTEND="none",
211221
=== modified file 'juju/hooks/protocol.py'
--- juju/hooks/protocol.py 2012-04-06 20:29:41 +0000
+++ juju/hooks/protocol.py 2012-06-15 02:00:25 +0000
@@ -42,14 +42,16 @@
4242
4343
44"""44"""
45import json45
46import logging46import logging
4747
48from twisted.internet import defer48from twisted.internet import defer
49from twisted.internet import protocol49from twisted.internet import protocol
50from twisted.protocols import amp50from twisted.protocols import amp
51import yaml
5152
52from juju.errors import JujuError53from juju.errors import JujuError
54from juju.lib.format import get_charm_formatter, get_charm_formatter_from_env
53from juju.state.errors import UnitRelationStateNotFound55from juju.state.errors import UnitRelationStateNotFound
54from juju.state.hook import RelationHookContext56from juju.state.hook import RelationHookContext
5557
@@ -96,14 +98,15 @@
96 ("relation_id", amp.String()),98 ("relation_id", amp.String()),
97 ("unit_name", amp.String()),99 ("unit_name", amp.String()),
98 ("setting_name", amp.String())]100 ("setting_name", amp.String())]
99 response = [("data", amp.String())]101 response = [("data", amp.String()),
102 ("charm_format", amp.Integer())]
100103
101104
102class RelationSetCommand(BaseCommand):105class RelationSetCommand(BaseCommand):
103 commandName = "relation_set"106 commandName = "relation_set"
104 arguments = [("client_id", amp.String()),107 arguments = [("client_id", amp.String()),
105 ("relation_id", amp.String()),108 ("relation_id", amp.String()),
106 ("json_blob", amp.String())]109 ("blob", amp.String())]
107 response = []110 response = []
108111
109112
@@ -184,7 +187,8 @@
184187
185 @RelationGetCommand.responder188 @RelationGetCommand.responder
186 @defer.inlineCallbacks189 @defer.inlineCallbacks
187 def relation_get(self, client_id, relation_id, unit_name, setting_name):190 def relation_get(self,
191 client_id, relation_id, unit_name, setting_name):
188 """Get settings from a state.hook.RelationHookContext192 """Get settings from a state.hook.RelationHookContext
189193
190 :param settings_name: optional setting_name (str) indicating that194 :param settings_name: optional setting_name (str) indicating that
@@ -192,11 +196,11 @@
192196
193 """197 """
194 context = self.factory.get_context(client_id)198 context = self.factory.get_context(client_id)
199 invoker = self.factory.get_invoker(client_id)
195 if relation_id:200 if relation_id:
196 yield self.factory.log(201 yield self.factory.log(
197 logging.DEBUG, "Getting relation %s" % relation_id)202 logging.DEBUG, "Getting relation %s" % relation_id)
198 context = yield self.factory.get_invoker(client_id).\203 context = yield invoker.get_relation_hook_context(relation_id)
199 get_relation_hook_context(relation_id)
200 require_relation_context(context)204 require_relation_context(context)
201205
202 try:206 try:
@@ -207,26 +211,32 @@
207 except UnitRelationStateNotFound, e:211 except UnitRelationStateNotFound, e:
208 raise NoSuchUnit(str(e))212 raise NoSuchUnit(str(e))
209213
210 defer.returnValue(dict(data=json.dumps(data)))214 formatter = get_charm_formatter(invoker.charm_format)
215 defer.returnValue(dict(
216 charm_format=invoker.charm_format,
217 data=formatter.dump(data)))
211218
212 @RelationSetCommand.responder219 @RelationSetCommand.responder
213 @defer.inlineCallbacks220 @defer.inlineCallbacks
214 def relation_set(self, client_id, relation_id, json_blob):221 def relation_set(self, client_id, relation_id, blob):
215 """Set values into state.hook.RelationHookContext.222 """Set values into state.hook.RelationHookContext.
216223
217 :param json_blob: a JSON serialized string of a dict that will224 :param blob: a YAML or JSON dumped string of a dict that will
218 contain the delta of settings to be applied to a unit_name.225 contain the delta of settings to be applied to a unit_name.
219 """226 """
220 data = json.loads(json_blob)
221 context = yield self.factory.get_context(client_id)227 context = yield self.factory.get_context(client_id)
228 invoker = self.factory.get_invoker(client_id)
229 formatter = get_charm_formatter(invoker.charm_format)
230 data = formatter.load(blob)
231
222 if relation_id:232 if relation_id:
223 yield self.factory.log(233 yield self.factory.log(
224 logging.DEBUG, "Setting relation %s" % relation_id)234 logging.DEBUG, "Setting relation %s" % relation_id)
225 context = yield self.factory.get_invoker(client_id).\235 context = yield invoker.get_relation_hook_context(relation_id)
226 get_relation_hook_context(relation_id)
227 require_relation_context(context)236 require_relation_context(context)
237
228 for k, v in data.items():238 for k, v in data.items():
229 if not v.strip():239 if formatter.should_delete(v):
230 yield context.delete_value(k)240 yield context.delete_value(k)
231 else:241 else:
232 yield context.set_value(k, v)242 yield context.set_value(k, v)
@@ -298,7 +308,9 @@
298 else:308 else:
299 options = dict(options)309 options = dict(options)
300310
301 defer.returnValue(dict(data=json.dumps(options)))311 # NOTE: no need to consider charm format for blob here, this
312 # blob has always been in YAML format
313 defer.returnValue(dict(data=yaml.safe_dump(options)))
302314
303 @OpenPortCommand.responder315 @OpenPortCommand.responder
304 @defer.inlineCallbacks316 @defer.inlineCallbacks
@@ -380,7 +392,8 @@
380 relation_id=relation_id,392 relation_id=relation_id,
381 unit_name=unit_name,393 unit_name=unit_name,
382 setting_name=setting_name)394 setting_name=setting_name)
383 defer.returnValue(json.loads(result["data"]))395 formatter = get_charm_formatter(result["charm_format"])
396 defer.returnValue(formatter.load(result["data"]))
384397
385 @defer.inlineCallbacks398 @defer.inlineCallbacks
386 def relation_set(self, client_id, relation_id, data):399 def relation_set(self, client_id, relation_id, data):
@@ -389,11 +402,12 @@
389 :param data: Python dict applied as a delta hook settings402 :param data: Python dict applied as a delta hook settings
390403
391 """404 """
392 json_blob = json.dumps(data)405 formatter = get_charm_formatter_from_env()
406 blob = formatter.dump(data)
393 yield self.callRemote(RelationSetCommand,407 yield self.callRemote(RelationSetCommand,
394 client_id=client_id,408 client_id=client_id,
395 relation_id=relation_id,409 relation_id=relation_id,
396 json_blob=json_blob)410 blob=blob)
397 defer.returnValue(None)411 defer.returnValue(None)
398412
399 @defer.inlineCallbacks413 @defer.inlineCallbacks
@@ -429,7 +443,7 @@
429 client_id=client_id,443 client_id=client_id,
430 option_name=option_name)444 option_name=option_name)
431 # Unbundle and deserialize445 # Unbundle and deserialize
432 result = json.loads(result["data"])446 result = yaml.safe_load(result["data"])
433 defer.returnValue(result)447 defer.returnValue(result)
434448
435 @defer.inlineCallbacks449 @defer.inlineCallbacks
436450
=== modified file 'juju/hooks/tests/test_cli.py'
--- juju/hooks/tests/test_cli.py 2012-03-27 01:04:50 +0000
+++ juju/hooks/tests/test_cli.py 2012-06-15 02:00:25 +0000
@@ -1,15 +1,17 @@
1# -*- encoding: utf-8 -*-
2
1import json3import json
2import logging4import logging
3import os5import os
4import StringIO6import StringIO
5from argparse import ArgumentTypeError7from argparse import ArgumentTypeError
8from contextlib import closing
69
7from twisted.internet.defer import inlineCallbacks, returnValue10from twisted.internet.defer import inlineCallbacks, returnValue
11import yaml
812
9from juju.errors import JujuError
10from juju.hooks.cli import (13from juju.hooks.cli import (
11 CommandLineClient, parse_keyvalue_pairs, parse_log_level,14 CommandLineClient, parse_log_level, parse_port_protocol)
12 parse_port_protocol)
13from juju.lib.testing import TestCase15from juju.lib.testing import TestCase
1416
1517
@@ -322,30 +324,6 @@
322 self.assertEquals(parse_log_level(logging.INFO), logging.INFO)324 self.assertEquals(parse_log_level(logging.INFO), logging.INFO)
323 self.assertEquals(parse_log_level(logging.ERROR), logging.ERROR)325 self.assertEquals(parse_log_level(logging.ERROR), logging.ERROR)
324326
325 def test_parse_keyvalue_pairs(self):
326 sample = self.makeFile("INPUT DATA")
327
328 # test various styles of options being read
329 options = ["alpha=beta",
330 "content=@%s" % sample]
331
332 data = parse_keyvalue_pairs(options)
333 self.assertEquals(data["alpha"], "beta")
334 self.assertEquals(data["content"], "INPUT DATA")
335
336 # and check an error condition
337 options = ["content=@missing"]
338 error = self.assertRaises(JujuError, parse_keyvalue_pairs, options)
339 self.assertEquals(
340 str(error),
341 "No such file or directory: missing (argument:content)")
342
343 # and check when fed non-kvpairs the error makes sense
344 options = ["foobar"]
345 error = self.assertRaises(JujuError, parse_keyvalue_pairs, options)
346 self.assertEquals(
347 str(error), "Expected `option=value`. Found `foobar`")
348
349 def test_parse_port_protocol(self):327 def test_parse_port_protocol(self):
350 self.assertEqual(parse_port_protocol("80"), (80, "tcp"))328 self.assertEqual(parse_port_protocol("80"), (80, "tcp"))
351 self.assertEqual(parse_port_protocol("443/tcp"), (443, "tcp"))329 self.assertEqual(parse_port_protocol("443/tcp"), (443, "tcp"))
@@ -388,33 +366,96 @@
388 "Invalid protocol, must be 'tcp' or 'udp', "366 "Invalid protocol, must be 'tcp' or 'udp', "
389 "got 'not-a-valid-protocol'")367 "got 'not-a-valid-protocol'")
390368
391 def test_format_smart(self):369 def assert_smart_output_v1(self, sample, formatted=object()):
392 cli = CommandLineClient()370 """Verifies output serialization"""
393371 # No roundtripping is verified because str(obj) is in general
394 output = StringIO.StringIO()372 # not roundtrippable
395 cli.format_smart(str('Basic string'), output)373 cli = CommandLineClient()
396 self.assertEqual(output.getvalue(), "Basic string\n")374 with closing(StringIO.StringIO()) as output:
397 output.close()375 cli.format_smart(sample, output)
398376 self.assertEqual(output.getvalue(), formatted)
399 output = StringIO.StringIO()377
400 cli.format_smart(float(1.0), output)378 def assert_format_smart_v1(self):
401 self.assertEqual(output.getvalue(), "1.0\n")379 """Verifies legacy smart format v1 which uses Python str encoding"""
402 output.close()380 self.assert_smart_output_v1(None, "") # No \n in output for None
403381 self.assert_smart_output_v1("", "\n")
404 output = StringIO.StringIO()382 self.assert_smart_output_v1("A string", "A string\n")
405 cli.format_smart(int(1), output)383 self.assert_smart_output_v1(
406 self.assertEqual(output.getvalue(), "1\n")384 "High bytes: \xca\xfe", "High bytes: \xca\xfe\n")
407 output.close()385 self.assert_smart_output_v1(u"", "\n")
408386 self.assert_smart_output_v1(
409 # LP bug # 900517 - smart format renders int(0) as an empty387 u"A unicode string (but really ascii)",
410 # string388 "A unicode string (but really ascii)\n")
411 output = StringIO.StringIO()389 # Maintain LP bug #901495, fixed in v2 format; this happens because
412 cli.format_smart(int(0), output)390 # str(obj) is used
413 self.assertEqual(output.getvalue(), "0\n")391 e = self.assertRaises(
414 output.close()392 UnicodeEncodeError,
415393 self.assert_smart_output_v1, u"中文")
416 # None does not even print the \n394 self.assertEqual(
417 output = StringIO.StringIO()395 str(e),
418 cli.format_smart(None, output)396 ("'ascii' codec can't encode characters in position 0-1: "
419 self.assertEqual(output.getvalue(), "")397 "ordinal not in range(128)"))
420 output.close()398 self.assert_smart_output_v1({}, "{}\n")
399 self.assert_smart_output_v1(
400 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com"},
401 "{u'public-address': u'ec2-1-2-3-4.compute-1.amazonaws.com'}\n")
402 self.assert_smart_output_v1(False, "False\n")
403 self.assert_smart_output_v1(True, "True\n")
404 self.assert_smart_output_v1(0.0, "0.0\n")
405 self.assert_smart_output_v1(3.14159, "3.14159\n")
406 self.assert_smart_output_v1(6.02214178e23, "6.02214178e+23\n")
407 self.assert_smart_output_v1(0, "0\n")
408 self.assert_smart_output_v1(42, "42\n")
409
410 def test_format_smart_v1_implied(self):
411 """Smart format v1 is implied if _JUJU_CHARM_FORMAT is not defined"""
412 # Double check env setup
413 self.assertNotIn("_JUJU_CHARM_FORMAT", os.environ)
414 self.assert_format_smart_v1()
415
416 def test_format_smart_v1(self):
417 """Verify legacy format v1 works"""
418 self.change_environment(_JUJU_CHARM_FORMAT="1")
419 self.assert_format_smart_v1()
420
421 def assert_smart_output(self, sample, formatted):
422 cli = CommandLineClient()
423 with closing(StringIO.StringIO()) as output:
424 cli.format_smart(sample, output)
425 self.assertEqual(output.getvalue(), formatted)
426 self.assertEqual(sample, yaml.safe_load(output.getvalue()))
427
428 def test_format_smart_v2(self):
429 """Verifies smart format v2 writes correct YAML"""
430 self.change_environment(_JUJU_CHARM_FORMAT="2")
431
432 # For each case, verify actual output serialization along with
433 # roundtripping through YAML
434 self.assert_smart_output(None, "") # No newline in output for None
435 self.assert_smart_output("", "''\n")
436 self.assert_smart_output("A string", "A string\n")
437 # Note: YAML uses b64 encoding for byte strings tagged by !!binary
438 self.assert_smart_output(
439 "High bytes: \xCA\xFE",
440 "!!binary |\n SGlnaCBieXRlczogyv4=\n")
441 self.assert_smart_output(u"", "''\n")
442 self.assert_smart_output(
443 u"A unicode string (but really ascii)",
444 "A unicode string (but really ascii)\n")
445 # Any non-ascii Unicode will use UTF-8 encoding
446 self.assert_smart_output(u"中文", "\xe4\xb8\xad\xe6\x96\x87\n")
447 self.assert_smart_output({}, "{}\n")
448 self.assert_smart_output(
449 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
450 u"foo": u"bar",
451 u"configured": True},
452 ("configured: true\n"
453 "foo: bar\n"
454 "public-address: ec2-1-2-3-4.compute-1.amazonaws.com\n"))
455 self.assert_smart_output(False, "false\n")
456 self.assert_smart_output(True, "true\n")
457 self.assert_smart_output(0.0, "0.0\n")
458 self.assert_smart_output(3.14159, "3.14159\n")
459 self.assert_smart_output(6.02214178e23, "6.02214178e+23\n")
460 self.assert_smart_output(0, "0\n")
461 self.assert_smart_output(42, "42\n")
421462
=== modified file 'juju/hooks/tests/test_communications.py'
--- juju/hooks/tests/test_communications.py 2012-04-06 20:29:41 +0000
+++ juju/hooks/tests/test_communications.py 2012-06-15 02:00:25 +0000
@@ -76,6 +76,12 @@
76 return MockServiceUnitState76 return MockServiceUnitState
7777
7878
79class MockInvoker(object):
80
81 def __init__(self, charm_format):
82 self.charm_format = charm_format
83
84
79class UnitSettingsFactoryLocal(UnitSettingsFactory):85class UnitSettingsFactoryLocal(UnitSettingsFactory):
80 """86 """
81 For testing a UnitSettingsFactory with local storage. Loosely87 For testing a UnitSettingsFactory with local storage. Loosely
@@ -91,6 +97,7 @@
91 self.members = []97 self.members = []
92 self.relation_idents = []98 self.relation_idents = []
93 self._agent_io = StringIO()99 self._agent_io = StringIO()
100 self._invoker = MockInvoker(charm_format=1)
94101
95 # hook context and a logger to the settings factory102 # hook context and a logger to the settings factory
96 logger = logging.getLogger("unit-settings-fact-test")103 logger = logging.getLogger("unit-settings-fact-test")
@@ -104,7 +111,7 @@
104 return self111 return self
105112
106 def invoker(self, client_id):113 def invoker(self, client_id):
107 return "mock invoker"114 return self._invoker
108115
109 def get_value(self, unit_name, setting_name):116 def get_value(self, unit_name, setting_name):
110 return self.data[unit_name][setting_name]117 return self.data[unit_name][setting_name]
@@ -310,7 +317,7 @@
310 error = MustSpecifyRelationName()317 error = MustSpecifyRelationName()
311 self.assertTrue(isinstance(error, JujuError))318 self.assertTrue(isinstance(error, JujuError))
312 self.assertEquals(319 self.assertEquals(
313 str(error), 320 str(error),
314 "Relation name must be specified")321 "Relation name must be specified")
315322
316 @defer.inlineCallbacks323 @defer.inlineCallbacks
317324
=== modified file 'juju/hooks/tests/test_invoker.py'
--- juju/hooks/tests/test_invoker.py 2012-05-04 03:36:36 +0000
+++ juju/hooks/tests/test_invoker.py 2012-06-15 02:00:25 +0000
@@ -1,3 +1,5 @@
1# -*- encoding: utf-8 -*-
2
1from StringIO import StringIO3from StringIO import StringIO
2import json4import json
3import logging5import logging
@@ -471,6 +473,7 @@
471473
472class InvokerTest(RelationInvokerTestBase):474class InvokerTest(RelationInvokerTestBase):
473475
476 @defer.inlineCallbacks
474 def test_environment(self):477 def test_environment(self):
475 """Test various way to manipulate the calling environment.478 """Test various way to manipulate the calling environment.
476 """479 """
@@ -494,11 +497,18 @@
494 self.assertEqual(env["DEBIAN_FRONTEND"], "noninteractive")497 self.assertEqual(env["DEBIAN_FRONTEND"], "noninteractive")
495 self.assertEqual(env["APT_LISTCHANGES_FRONTEND"], "none")498 self.assertEqual(env["APT_LISTCHANGES_FRONTEND"], "none")
496499
500 # Specific to the charm that is running, in this case it's the
501 # dummy charm (this is the default charm used when the
502 # add_service method is use)
503 self.assertEqual(env["_JUJU_CHARM_FORMAT"], "1")
504
505 @defer.inlineCallbacks
497 def test_missing_hook(self):506 def test_missing_hook(self):
498 exe = yield self.ua.get_invoker(507 exe = yield self.ua.get_invoker(
499 "database:42", "add", "mysql/0", self.relation)508 "database:42", "add", "mysql/0", self.relation)
500 self.failUnlessRaises(errors.FileNotFound, exe, "hook-missing")509 self.failUnlessRaises(errors.FileNotFound, exe, "hook-missing")
501510
511 @defer.inlineCallbacks
502 def test_noexec_hook(self):512 def test_noexec_hook(self):
503 exe = yield self.ua.get_invoker(513 exe = yield self.ua.get_invoker(
504 "database:42", "add", "mysql/0", self.relation)514 "database:42", "add", "mysql/0", self.relation)
@@ -1080,10 +1090,10 @@
1080 u"b": u"xyz",1090 u"b": u"xyz",
1081 "private-address": "mysql-0.example.com"})1091 "private-address": "mysql-0.example.com"})
1082 yield self.assert_zk_data(db1, {1092 yield self.assert_zk_data(db1, {
1083 u"c": u"21",1093 u"c": "21",
1084 "private-address": "mysql-0.example.com"})1094 "private-address": "mysql-0.example.com"})
1085 yield self.assert_zk_data(db2, {1095 yield self.assert_zk_data(db2, {
1086 u"d": u"99",1096 u"d": "99",
1087 "private-address": "mysql-0.example.com"})1097 "private-address": "mysql-0.example.com"})
1088 self.assertLogLines(1098 self.assertLogLines(
1089 self.log.getvalue(),1099 self.log.getvalue(),
@@ -1502,3 +1512,166 @@
1502 self.assertIn("mysql/1", self.log.getvalue())1512 self.assertIn("mysql/1", self.log.getvalue())
1503 # we don't see units in the other container1513 # we don't see units in the other container
1504 self.assertNotIn("mysql/0", self.log.getvalue())1514 self.assertNotIn("mysql/0", self.log.getvalue())
1515
1516
1517def capture_separate_log(name, level):
1518 """Support the separate capture of logging at different log levels.
1519
1520 TestCase.capture_logging only allows one level to be captured at
1521 any given time. Given that the hook log captures both data AND
1522 traditional logging, it's useful to separate.
1523 """
1524 logger = logging.getLogger(name)
1525 output = StringIO()
1526 handler = logging.StreamHandler(output)
1527 handler.setLevel(level)
1528 logger.addHandler(handler)
1529 return output
1530
1531
1532class TestCharmFormatV1(RelationInvokerTestBase):
1533
1534 @defer.inlineCallbacks
1535 def _default_relations(self):
1536 """Intercept mysql/wordpress setup to ensure v1 charm format"""
1537 # The mysql charm in the test repository has no format defined
1538 yield self.add_service_from_charm("mysql", charm_name="mysql")
1539 yield super(TestCharmFormatV1, self)._default_relations()
1540
1541 @defer.inlineCallbacks
1542 def test_environment(self):
1543 """Ensure that an implicit setting of format: 1 works properly"""
1544 exe = yield self.ua.get_invoker(
1545 "database:42", "add", "mysql/0", self.relation)
1546 env = exe.get_environment()
1547 self.assertEqual(env["_JUJU_CHARM_FORMAT"], "1")
1548
1549 @defer.inlineCallbacks
1550 def test_output(self):
1551 """Verify roundtripping"""
1552 hook_debug_log = capture_separate_log("hook", level=logging.DEBUG)
1553 hook_log = capture_separate_log("hook", level=logging.INFO)
1554 exe = yield self.ua.get_invoker(
1555 "database:42", "add", "mysql/0", self.relation,
1556 client_id="client_id")
1557 set_hook = self.create_hook(
1558 "relation-set",
1559 "b=true i=42 f=1.23 s=ascii u=中文")
1560 yield exe(set_hook)
1561 result = yield exe(self.create_hook("relation-get", "- mysql/0"))
1562 self.assertEqual(result, 0)
1563
1564 # No guarantee on output ordering, so keep to this test stable,
1565 # first a little parsing work:
1566 output = hook_log.getvalue()
1567 self.assertEqual(output[0], "{")
1568 self.assertEqual(output[-3:-1], "}\n")
1569 self.assertEqual(
1570 set(output[1:-3].split(", ")),
1571 set(["u'b': u'true'",
1572 "u'f': u'1.23'",
1573 "u'i': u'42'",
1574 "u'private-address': u'mysql-0.example.com'",
1575 "u's': u'ascii'",
1576 "u'u': u'\\u4e2d\\u6587'"]))
1577
1578 self.assertLogLines(
1579 hook_debug_log.getvalue(),
1580 ["Flushed values for hook %r on 'database:42'" % (
1581 os.path.basename(set_hook),),
1582 " Setting changed: u'b'=u'true' (was unset)",
1583 " Setting changed: u'f'=u'1.23' (was unset)",
1584 " Setting changed: u'i'=u'42' (was unset)",
1585 " Setting changed: u's'=u'ascii' (was unset)",
1586 " Setting changed: u'u'=u'\\u4e2d\\u6587' (was unset)"])
1587
1588 # Unlike v2 formatting, this will only fail on output
1589 hook_log.truncate()
1590 data_file = self.makeFile("But when I do drink, I prefer \xCA\xFE")
1591 yield exe(self.create_hook(
1592 "relation-set", "d=@%s" % data_file))
1593 result = yield exe(self.create_hook("relation-get", "d mysql/0"))
1594 self.assertIn(
1595 "Error: \'utf8\' codec can\'t decode byte 0xca in position 30: "
1596 "invalid continuation byte",
1597 hook_log.getvalue())
1598
1599
1600class TestCharmFormatV2(RelationInvokerTestBase):
1601
1602 @defer.inlineCallbacks
1603 def _default_relations(self):
1604 """Intercept mysql/wordpress setup to ensure v2 charm format"""
1605 # The mysql-format-v2 charm defines format:2 in its metadata
1606 yield self.add_service_from_charm(
1607 "mysql", charm_name="mysql-format-v2")
1608 yield super(TestCharmFormatV2, self)._default_relations()
1609
1610 @defer.inlineCallbacks
1611 def test_environment(self):
1612 """Ensure that an explicit setting of format: 2 works properly"""
1613 exe = yield self.ua.get_invoker(
1614 "database:42", "add", "mysql/0", self.relation)
1615 env = exe.get_environment()
1616 self.assertEqual(env["_JUJU_CHARM_FORMAT"], "2")
1617
1618 @defer.inlineCallbacks
1619 def test_output(self):
1620 """Verify roundtripping"""
1621 hook_debug_log = capture_separate_log("hook", level=logging.DEBUG)
1622 hook_log = capture_separate_log("hook", level=logging.INFO)
1623 exe = yield self.ua.get_invoker(
1624 "database:42", "add", "mysql/0", self.relation,
1625 client_id="client_id")
1626
1627 # Byte strings are also supported by staying completely in
1628 # YAML, so test that corner case. This also means users need
1629 # to present valid YAML for any input:
1630 data_file = self.makeFile(
1631 yaml.safe_dump("But when I do drink, I prefer \xCA\xFE"))
1632 set_hook = self.create_hook(
1633 "relation-set",
1634 "b=true i=42 f=1.23 s=ascii u=中文 d=@%s" % data_file)
1635 yield exe(set_hook)
1636 result = yield exe(self.create_hook("relation-get", "- mysql/0"))
1637 self.assertEqual(result, 0)
1638
1639 # YAML guarantees that the keys will be sorted
1640 # lexicographically; note that we output UTF-8 for Unicode
1641 # when dumping YAML, so our source text (with this test file
1642 # in UTF-8 itself) matches the output text, as seen in the
1643 # characters for "zhongwen" (Chinese language).
1644 self.assertEqual(
1645 hook_log.getvalue(),
1646 "b: true\n"
1647 "d: !!binary |\n QnV0IHdoZW4gSSBkbyBkcmluaywgSSBwcmVmZXIgyv4=\n"
1648 "f: 1.23\n"
1649 "i: 42\n"
1650 "private-address: mysql-0.example.com\n"
1651 "s: ascii\n"
1652 "u: 中文\n\n")
1653
1654 # Log lines are not simply converted into Unicode, as in v1 format
1655 self.assertLogLines(
1656 hook_debug_log.getvalue(),
1657 ["Flushed values for hook %r on 'database:42'" % (
1658 os.path.basename(set_hook),),
1659 " Setting changed: 'b'=True (was unset)",
1660 " Setting changed: 'd'='But when I do drink, "
1661 "I prefer \\xca\\xfe' (was unset)",
1662 " Setting changed: 'f'=1.23 (was unset)",
1663 " Setting changed: 'i'=42 (was unset)",
1664 " Setting changed: 's'='ascii' (was unset)",
1665 " Setting changed: 'u'=u'\\u4e2d\\u6587' (was unset)"])
1666
1667 # Also ensure that invalid YAML is rejected; unlike earlier,
1668 # this was not encoded with yaml.safe_dump
1669 data_file = self.makeFile(
1670 "But when I do drink, I prefer \xCA\xFE")
1671 hook = self.create_hook("relation-set", "d=@%s" % data_file)
1672 e = yield self.assertFailure(exe(hook), errors.CharmInvocationError)
1673 self.assertEqual(str(e), "Error processing %r: exit code 1." % hook)
1674 self.assertIn(
1675 "yaml.reader.ReaderError: \'utf8\' codec can\'t decode byte #xca: "
1676 "invalid continuation byte\n in \"<string>\", position 30",
1677 hook_log.getvalue())
15051678
=== added file 'juju/lib/format.py'
--- juju/lib/format.py 1970-01-01 00:00:00 +0000
+++ juju/lib/format.py 2012-06-15 02:00:25 +0000
@@ -0,0 +1,144 @@
1"""Utility functions and constants to support uniform i/o formatting."""
2
3import json
4import os
5
6import yaml
7
8from juju.errors import JujuError
9
10
11class BaseFormat(object):
12 """Maintains parallel code paths for input and output formatting
13 through the subclasses Format1 (Python str formatting with JSON
14 encoding) and Format2 (YAML format).
15 """
16
17 def parse_keyvalue_pairs(self, pairs):
18 """Parses key value pairs, using `_parse_value` for specific format"""
19 data = {}
20 for kv in pairs:
21 if "=" not in kv:
22 raise JujuError(
23 "Expected `option=value`. Found `%s`" % kv)
24
25 k, v = kv.split("=", 1)
26 if v.startswith("@"):
27 # Handle file input, any parsing/sanitization is done next
28 # with respect to charm format
29 filename = v[1:]
30 try:
31 with open(filename, "r") as f:
32 v = f.read()
33 except IOError:
34 raise JujuError(
35 "No such file or directory: %s (argument:%s)" % (
36 filename,
37 k))
38 except Exception, e:
39 raise JujuError("Bad file %s" % e)
40
41 data[k] = self._parse_value(k, v)
42
43 return data
44
45
46class Format1(BaseFormat):
47 """Supports backwards compatibility through str and JSON encoding."""
48
49 charm_format = 1
50
51 def format(self, data):
52 """Formats `data` using Python str encoding"""
53 return str(data)
54
55 def _parse_value(self, key, value):
56 """Interprets value as a str"""
57 return value
58
59 # For the old format: 1, using JSON serialization introduces some
60 # subtle issues around Unicode conversion that then later results
61 # in bugginess. For compatibility reasons, we keep these old bugs
62 # around, by dumping and loading into JSON at appropriate points.
63
64 def dump(self, data):
65 """Dumps using JSON serialization"""
66 return json.dumps(data)
67
68 def load(self, data):
69 """Loads data, but also converts str to Unicode"""
70 return json.loads(data)
71
72 def should_delete(self, value):
73 """Whether `value` implies corresponding key should be deleted"""
74 # In format: 1, all values are strings, but possibly with
75 # spaces. The strip reduces strings consisting only of spaces,
76 # or otherwise empty, to an empty string.
77 return not value.strip()
78
79
80class Format2(BaseFormat):
81 """New format that uses YAML, so no unexpected encoding issues"""
82
83 charm_format = 2
84
85 def format(self, data):
86 """Formats `data` in Juju's preferred YAML format"""
87 # Return value such that it roundtrips; this allows us to
88 # report back the boolean false instead of the Python
89 # output format, False
90 if data is None:
91 return ""
92 serialized = yaml.safe_dump(
93 data, indent=4, default_flow_style=False, width=80,
94 allow_unicode=True)
95 if serialized.endswith("\n...\n"):
96 # Remove explicit doc end sentinel, still valid yaml
97 serialized = serialized[0:-5]
98 # Also remove any extra \n, will still be valid yaml
99 return serialized.rstrip("\n")
100
101 def _parse_value(self, key, value):
102 # Ensure roundtripping to/from yaml if format: 2; in
103 # particular this ensures that true/false work as desired
104 try:
105 return yaml.safe_load(value)
106 except Exception:
107 raise JujuError("Invalid YAML value (argument:%s)" % key)
108
109 # Use the same format for dump
110 dump = format
111
112 def load(self, data):
113 """Loads data safely, ensuring no Python specific type info leaks"""
114 return yaml.safe_load(data)
115
116 def should_delete(self, value):
117 """Whether `value` implies corresponding key should be deleted"""
118 # In format: 2, values were already parsed by yaml.safe_load;
119 # in particular, a string consisting only of whitespace is
120 # parsed as None.
121 return value is None
122
123
124def is_valid_charm_format(charm_format):
125 """True if `charm_format` is a valid format"""
126 return charm_format in (Format1.charm_format, Format2.charm_format)
127
128
129def get_charm_formatter(charm_format):
130 """Map `charm_format` to the implementing strategy for that format"""
131 if charm_format == Format1.charm_format:
132 return Format1()
133 elif charm_format == Format2.charm_format:
134 return Format2()
135 else:
136 raise JujuError(
137 "Expected charm format to be either 1 or 2, got %s" % (
138 charm_format,))
139
140
141def get_charm_formatter_from_env():
142 """Return the formatter specified by $_JUJU_CHARM_FORMAT"""
143 return get_charm_formatter(int(
144 os.environ.get("_JUJU_CHARM_FORMAT", "1")))
0145
=== added file 'juju/lib/tests/test_format.py'
--- juju/lib/tests/test_format.py 1970-01-01 00:00:00 +0000
+++ juju/lib/tests/test_format.py 2012-06-15 02:00:25 +0000
@@ -0,0 +1,330 @@
1# -*- encoding: utf-8 -*-
2
3import json
4
5import yaml
6
7from juju.errors import JujuError
8from juju.lib.testing import TestCase
9from juju.lib.format import (
10 Format1, Format2, is_valid_charm_format, get_charm_formatter,
11 get_charm_formatter_from_env)
12
13
14class TestFormatLookup(TestCase):
15
16 def test_is_valid_charm_format(self):
17 """Verify currently valid charm formats"""
18 self.assertFalse(is_valid_charm_format(0))
19 self.assertTrue(is_valid_charm_format(1))
20 self.assertTrue(is_valid_charm_format(2))
21 self.assertFalse(is_valid_charm_format(3))
22
23 def test_get_charm_formatter(self):
24 self.assertInstance(get_charm_formatter(1), Format1)
25 self.assertInstance(get_charm_formatter(2), Format2)
26 e = self.assertRaises(JujuError, get_charm_formatter, 0)
27 self.assertEqual(
28 str(e),
29 "Expected charm format to be either 1 or 2, got 0")
30
31 def test_get_charm_formatter_from_env(self):
32 """Verifies _JUJU_CHARM_FORMAT can be mapped to valid formatters"""
33
34 self.change_environment(_JUJU_CHARM_FORMAT="0")
35 e = self.assertRaises(JujuError, get_charm_formatter_from_env)
36 self.assertEqual(
37 str(e),
38 "Expected charm format to be either 1 or 2, got 0")
39
40 self.change_environment(_JUJU_CHARM_FORMAT="1")
41 self.assertInstance(get_charm_formatter_from_env(), Format1)
42
43 self.change_environment(_JUJU_CHARM_FORMAT="2")
44 self.assertInstance(get_charm_formatter_from_env(), Format2)
45
46 self.change_environment(_JUJU_CHARM_FORMAT="3")
47 e = self.assertRaises(JujuError, get_charm_formatter_from_env)
48 self.assertEqual(
49 str(e),
50 "Expected charm format to be either 1 or 2, got 3")
51
52
53class TestFormat1(TestCase):
54
55 def assert_format(self, data, expected):
56 """Verify str output serialization; no roundtripping is supported."""
57 formatted = Format1().format(data)
58 self.assertEqual(formatted, expected)
59
60 def test_format(self):
61 """Verifies Python str formatting of data"""
62 self.assert_format(None, "None")
63 self.assert_format("", "")
64 self.assert_format("A string", "A string")
65 self.assert_format(
66 "High bytes: \xCA\xFE",
67 "High bytes: \xca\xfe")
68 self.assert_format(u"", "")
69 self.assert_format(
70 u"A unicode string (but really ascii)",
71 "A unicode string (but really ascii)")
72 e = self.assertRaises(UnicodeEncodeError, Format1().format, u"中文")
73 self.assertEqual(
74 str(e),
75 ("'ascii' codec can't encode characters in position 0-1: "
76 "ordinal not in range(128)"))
77 self.assert_format({}, "{}")
78 self.assert_format(
79 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
80 u"foo": u"bar",
81 u"configured": True},
82 ("{u'public-address': u'ec2-1-2-3-4.compute-1.amazonaws.com', "
83 "u'foo': u'bar', u'configured': True}"))
84 self.assert_format(False, "False")
85 self.assert_format(True, "True")
86 self.assert_format(0.0, "0.0")
87 self.assert_format(3.14159, "3.14159")
88 self.assert_format(6.02214178e23, "6.02214178e+23")
89 self.assert_format(0, "0")
90 self.assert_format(42, "42")
91
92 def test_parse_keyvalue_pairs(self):
93 """Verify reads in values as strings"""
94 sample = self.makeFile("INPUT DATA")
95
96 # test various styles of options being read
97 options = ["alpha=beta",
98 "content=@%s" % sample]
99
100 formatter = Format1()
101 data = formatter.parse_keyvalue_pairs(options)
102 self.assertEquals(data["alpha"], "beta")
103 self.assertEquals(data["content"], "INPUT DATA")
104
105 # and check an error condition
106 options = ["content=@missing"]
107 error = self.assertRaises(
108 JujuError, formatter.parse_keyvalue_pairs, options)
109 self.assertEquals(
110 str(error),
111 "No such file or directory: missing (argument:content)")
112
113 # and check when fed non-kvpairs the error makes sense
114 options = ["foobar"]
115 error = self.assertRaises(
116 JujuError, formatter.parse_keyvalue_pairs, options)
117 self.assertEquals(
118 str(error), "Expected `option=value`. Found `foobar`")
119
120 def assert_dump_load(self, data, expected):
121 """Asserts expected formatting and roundtrip through dump/load"""
122 formatter = Format1()
123 dumped = formatter.dump({"data": data})
124 loaded = formatter.load(dumped)["data"]
125 self.assertEqual(dumped, expected)
126 self.assertEqual(dumped, json.dumps({"data": data}))
127 self.assertEqual(data, loaded)
128 if isinstance(data, str):
129 # Verify promotion of str to unicode
130 self.assertInstance(loaded, unicode)
131
132 def test_dump_load(self):
133 """Verify JSON roundtrip semantics"""
134 self.assert_dump_load(None, '{"data": null}')
135 self.assert_dump_load("", '{"data": ""}')
136 self.assert_dump_load("A string", '{"data": "A string"}')
137 e = self.assertRaises(
138 UnicodeDecodeError,
139 Format1().dump, "High bytes: \xCA\xFE")
140 self.assertEqual(
141 str(e),
142 "'utf8' codec can't decode byte 0xca in position 12: "
143 "invalid continuation byte")
144 self.assert_dump_load(u"", '{"data": ""}')
145 self.assert_dump_load(
146 u"A unicode string (but really ascii)",
147 '{"data": "A unicode string (but really ascii)"}')
148 e = self.assertRaises(UnicodeEncodeError, Format1().format, u"中文")
149 self.assertEqual(
150 str(e),
151 ("'ascii' codec can't encode characters in position 0-1: "
152 "ordinal not in range(128)"))
153 self.assert_dump_load({}, '{"data": {}}')
154 self.assert_dump_load(
155 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
156 u"foo": u"bar",
157 u"configured": True},
158 ('{"data": {"public-address": '
159 '"ec2-1-2-3-4.compute-1.amazonaws.com", "foo": "bar", '
160 '"configured": true}}'))
161 self.assert_dump_load(False, '{"data": false}')
162 self.assert_dump_load(True, '{"data": true}')
163 self.assert_dump_load(0.0, '{"data": 0.0}')
164 self.assert_dump_load(3.14159, '{"data": 3.14159}')
165 self.assert_dump_load(6.02214178e23, '{"data": 6.02214178e+23}')
166 self.assert_dump_load(0, '{"data": 0}')
167 self.assert_dump_load(42, '{"data": 42}')
168
169 def test_should_delete(self):
170 """Verify empty or whitespace only strings indicate deletion"""
171 formatter = Format1()
172 self.assertFalse(formatter.should_delete("0"))
173 self.assertFalse(formatter.should_delete("something"))
174 self.assertTrue(formatter.should_delete(""))
175 self.assertTrue(formatter.should_delete(" "))
176
177 # Verify that format: 1 can only work with str values
178 e = self.assertRaises(AttributeError, formatter.should_delete, 42)
179 self.assertEqual(str(e), "'int' object has no attribute 'strip'")
180 e = self.assertRaises(AttributeError, formatter.should_delete, None)
181 self.assertEqual(str(e), "'NoneType' object has no attribute 'strip'")
182
183
184class TestFormat2(TestCase):
185
186 def assert_format(self, data, expected):
187 """Verify actual output serialization and roundtripping through YAML"""
188 formatted = Format2().format(data)
189 self.assertEqual(formatted, expected)
190 self.assertEqual(data, yaml.safe_load(formatted))
191
192 def test_format(self):
193 """Verifies standard formatting of data into valid YAML"""
194 self.assert_format(None, "")
195 self.assert_format("", "''")
196 self.assert_format("A string", "A string")
197 # Note: YAML uses b64 encoding for byte strings tagged by !!binary
198 self.assert_format(
199 "High bytes: \xCA\xFE",
200 "!!binary |\n SGlnaCBieXRlczogyv4=")
201 self.assert_format(u"", "''")
202 self.assert_format(
203 u"A unicode string (but really ascii)",
204 "A unicode string (but really ascii)")
205 # Any non-ascii Unicode will use UTF-8 encoding
206 self.assert_format(u"中文", "\xe4\xb8\xad\xe6\x96\x87")
207 self.assert_format({}, "{}")
208 self.assert_format(
209 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
210 u"foo": u"bar",
211 u"configured": True},
212 ("configured: true\n"
213 "foo: bar\n"
214 "public-address: ec2-1-2-3-4.compute-1.amazonaws.com"))
215 self.assert_format(False, "false")
216 self.assert_format(True, "true")
217 self.assert_format(0.0, "0.0")
218 self.assert_format(3.14159, "3.14159")
219 self.assert_format(6.02214178e23, "6.02214178e+23")
220 self.assert_format(0, "0")
221 self.assert_format(42, "42")
222
223 def assert_parse(self, data):
224 """Verify input parses as expected, including from a data file"""
225 formatter = Format2()
226 formatted = formatter.format(data)
227 data_file = self.makeFile(formatted)
228 kvs = ["formatted=%s" % formatted,
229 "file=@%s" % data_file]
230 parsed = formatter.parse_keyvalue_pairs(kvs)
231 self.assertEqual(parsed["formatted"], data)
232 self.assertEqual(parsed["file"], data)
233
234 def test_parse_keyvalue_pairs(self):
235 """Verify key value pairs parse for a wide range of YAML inputs."""
236 formatter = Format2()
237 self.assert_parse(None)
238 self.assert_parse("")
239 self.assert_parse("A string")
240 self.assert_parse("High bytes: \xCA\xFE")
241 self.assert_parse(u"")
242 self.assert_parse(u"A unicode string (but really ascii)")
243 self.assert_parse(u"中文")
244 self.assert_parse({})
245 self.assert_parse(
246 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
247 u"foo": u"bar",
248 u"configured": True})
249 self.assert_parse([])
250 self.assert_parse(["abc", "xyz", 42, True])
251 self.assert_parse(False)
252 self.assert_parse(True)
253 self.assert_parse(0.0)
254 self.assert_parse(3.14159)
255 self.assert_parse(6.02214178e23)
256 self.assert_parse(0)
257 self.assert_parse(42)
258
259 # Raises an error if no such file
260 e = self.assertRaises(
261 JujuError,
262 formatter.parse_keyvalue_pairs, ["content=@missing"])
263 self.assertEquals(
264 str(e),
265 "No such file or directory: missing (argument:content)")
266
267 # Raises an error if not of the form K=V or K=
268 e = self.assertRaises(
269 JujuError,
270 formatter.parse_keyvalue_pairs, ["foobar"])
271 self.assertEquals(
272 str(e), "Expected `option=value`. Found `foobar`")
273
274 # Raises an error if the value is invalid YAML
275 e = self.assertRaises(
276 JujuError,
277 formatter.parse_keyvalue_pairs, ["content=\xCA\FE"])
278 self.assertEquals(
279 str(e),
280 "Invalid YAML value (argument:content)")
281
282 def assert_dump_load(self, data, expected):
283 """Asserts expected formatting and roundtrip through dump/load"""
284 formatter = Format2()
285 dumped = formatter.dump({"data": data})
286 loaded = formatter.load(dumped)["data"]
287 self.assertEqual(dumped, expected)
288 self.assertEqual(data, loaded)
289 if isinstance(data, str):
290 # Verify that no promotion to unicode occurs for str values
291 self.assertInstance(loaded, str)
292
293 def test_dump_load(self):
294 """Verify JSON roundtrip semantics"""
295 self.assert_dump_load(None, "data: null")
296 self.assert_dump_load("", "data: ''")
297 self.assert_dump_load("A string", "data: A string")
298 self.assert_dump_load("High bytes: \xCA\xFE",
299 "data: !!binary |\n SGlnaCBieXRlczogyv4=")
300 self.assert_dump_load(u"", "data: ''")
301 self.assert_dump_load(
302 u"A unicode string (but really ascii)",
303 "data: A unicode string (but really ascii)")
304 self.assert_dump_load(u"中文", "data: \xe4\xb8\xad\xe6\x96\x87")
305 self.assert_dump_load({}, "data: {}")
306 self.assert_dump_load(
307 {u"public-address": u"ec2-1-2-3-4.compute-1.amazonaws.com",
308 u"foo": u"bar",
309 u"configured": True},
310 ("data:\n"
311 " configured: true\n"
312 " foo: bar\n"
313 " public-address: ec2-1-2-3-4.compute-1.amazonaws.com"))
314 self.assert_dump_load(False, "data: false")
315 self.assert_dump_load(True, "data: true")
316 self.assert_dump_load(0.0, "data: 0.0")
317 self.assert_dump_load(3.14159, "data: 3.14159")
318 self.assert_dump_load(6.02214178e23, "data: 6.02214178e+23")
319 self.assert_dump_load(0, "data: 0")
320 self.assert_dump_load(42, "data: 42")
321
322 def test_should_delete(self):
323 """Verify only `None` values (as YAML loaded) indicate deletion"""
324 formatter = Format2()
325 self.assertFalse(formatter.should_delete("0"))
326 self.assertFalse(formatter.should_delete("something"))
327 self.assertFalse(formatter.should_delete(""))
328 self.assertFalse(formatter.should_delete(" "))
329 self.assertFalse(formatter.should_delete(0))
330 self.assertTrue(formatter.should_delete(None))
0331
=== modified file 'juju/state/tests/test_relation.py'
--- juju/state/tests/test_relation.py 2012-05-15 19:07:47 +0000
+++ juju/state/tests/test_relation.py 2012-06-15 02:00:25 +0000
@@ -12,6 +12,7 @@
12from juju.charm.directory import CharmDirectory12from juju.charm.directory import CharmDirectory
13from juju.charm.tests import local_charm_id13from juju.charm.tests import local_charm_id
14from juju.machine.tests.test_constraints import dummy_constraints14from juju.machine.tests.test_constraints import dummy_constraints
15from juju.charm.tests.test_metadata import test_repository_path
15from juju.charm.tests.test_repository import unbundled_repository16from juju.charm.tests.test_repository import unbundled_repository
16from juju.lib.pick import pick_attr17from juju.lib.pick import pick_attr
17from juju.state.charm import CharmStateManager18from juju.state.charm import CharmStateManager
@@ -52,6 +53,26 @@
52 returnValue(service_state)53 returnValue(service_state)
5354
54 @inlineCallbacks55 @inlineCallbacks
56 def add_service_from_charm(
57 self, service_name, charm_id=None, constraints=None,
58 charm_dir=None, charm_name=None):
59 """Add a service from a charm.
60 """
61 if not charm_id and charm_dir is None:
62 charm_name = charm_name or service_name
63 charm_dir = CharmDirectory(os.path.join(
64 test_repository_path, "series", charm_name))
65 if charm_id is None:
66 charm_state = yield self.charm_manager.add_charm_state(
67 local_charm_id(charm_dir), charm_dir, "")
68 else:
69 charm_state = yield self.charm_manager.get_charm_state(
70 charm_id)
71 service_state = yield self.service_manager.add_service_state(
72 service_name, charm_state, constraints or dummy_constraints)
73 returnValue(service_state)
74
75 @inlineCallbacks
55 def add_relation(self, relation_type, *services):76 def add_relation(self, relation_type, *services):
56 """Support older tests that don't use `RelationEndpoint`s"""77 """Support older tests that don't use `RelationEndpoint`s"""
57 endpoints = []78 endpoints = []
@@ -471,8 +492,9 @@
471 "logging", "juju-info", "juju-info", "client", "container")492 "logging", "juju-info", "juju-info", "client", "container")
472 yield self.add_service("mysql")493 yield self.add_service("mysql")
473 yield self.add_service("logging")494 yield self.add_service("logging")
474 relation_state, service_states = yield self.relation_manager.add_relation_state(495 relation_state, service_states = \
475 mysql_ep, logging_ep)496 yield self.relation_manager.add_relation_state(mysql_ep,
497 logging_ep)
476498
477 # verify that the relation state instances are both marked499 # verify that the relation state instances are both marked
478 # with the live scope (container). This happens even though500 # with the live scope (container). This happens even though
@@ -488,8 +510,9 @@
488 "logging", "juju-info", "juju-info", "client", "container")510 "logging", "juju-info", "juju-info", "client", "container")
489 yield self.add_service("mysql")511 yield self.add_service("mysql")
490 yield self.add_service("logging")512 yield self.add_service("logging")
491 relation_state, service_states = yield self.relation_manager.add_relation_state(513 relation_state, service_states = \
492 mysql_ep, logging_ep)514 yield self.relation_manager.add_relation_state(mysql_ep,
515 logging_ep)
493516
494 for service_state in service_states:517 for service_state in service_states:
495 self.assertEqual(service_state.relation_scope, "container")518 self.assertEqual(service_state.relation_scope, "container")
@@ -502,8 +525,9 @@
502 "logging", "juju-info", "juju-info", "client", "global")525 "logging", "juju-info", "juju-info", "client", "global")
503 yield self.add_service("mysql")526 yield self.add_service("mysql")
504 yield self.add_service("logging")527 yield self.add_service("logging")
505 relation_state, service_states = yield self.relation_manager.add_relation_state(528 relation_state, service_states = \
506 mysql_ep, logging_ep)529 yield self.relation_manager.add_relation_state(mysql_ep,
530 logging_ep)
507531
508 for service_state in service_states:532 for service_state in service_states:
509 self.assertEqual(service_state.relation_scope, "container")533 self.assertEqual(service_state.relation_scope, "container")
@@ -693,7 +717,8 @@
693 id = "relation-0000000000"717 id = "relation-0000000000"
694 self.assertEqual(718 self.assertEqual(
695 repr(self.service1_relation),719 repr(self.service1_relation),
696 "<ServiceRelationState name:dev-connect role:prod id:%s scope:global>" % id)720 "<ServiceRelationState name:dev-connect "
721 "role:prod id:%s scope:global>" % id)
697722
698 def test_property_relation_name(self):723 def test_property_relation_name(self):
699 """724 """

Subscribers

People subscribed via source and target branches

to status/vote changes: