Merge lp:~rharding/charmworld/proof-config into lp:~juju-jitsu/charmworld/trunk

Proposed by Richard Harding
Status: Merged
Merged at revision: 424
Proposed branch: lp:~rharding/charmworld/proof-config
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 533 lines (+412/-44)
4 files modified
charmworld/lib/proof.py (+76/-0)
charmworld/lib/tests/test_proof.py (+151/-0)
charmworld/views/api.py (+87/-39)
charmworld/views/tests/test_api.py (+98/-5)
To merge this branch: bzr merge lp:~rharding/charmworld/proof-config
Reviewer Review Type Date Requested Status
Juju-Jitsu Hackers Pending
Review via email: mp+191689@code.launchpad.net

Description of the change

Add charm config proofing and update api response

- Move proof logic into a new lib/proof.py module
- Update the proof api call to look through the bundles and proof them
- Change errors to be catchable exceptions, the exception contains both a msg
and debug_info we expose via the api call
- Update the api call to provide a summary error_messages (for Marco's needs)
but keep the details in the actual list of errors with the messages and the
debug info we have that went into those errors.

Sample proof api call and response:
http://paste.mitechie.com/show/1049/

Note: this is only adding the new proof checks of validating the
config/options from the charm found in the database to the options defined in
the bundle.

- Validate the option exists in the charm we found in the db
- Validate that the type is of the correct type.
- Validate that the charm we found has options at all.

QA
---
You have to ingest the charms so that we can validate them. You can then toss
the yaml file (as a single string) to the proof api endpoint as a POST'd key
deployer_file. Right now it only handles yaml.

https://codereview.appspot.com/14789043/

To post a comment you must log in.
lp:~rharding/charmworld/proof-config updated
432. By Richard Harding

Fix for the updated error_messages

433. By Richard Harding

add bundle proof tests

434. By Richard Harding

Add proof tests for the CharmProof

435. By Richard Harding

Add support for checking the updated proofing call

436. By Richard Harding

Update test to be a bit better demo of multiple errors

437. By Richard Harding

lint

Revision history for this message
Richard Harding (rharding) wrote :
Download full text (3.6 KiB)

Reviewers: mp+191689_code.launchpad.net,

Message:
*** Submitted:

Add charm config proofing and update api response

- Move proof logic into a new lib/proof.py module
- Update the proof api call to look through the bundles and proof them
- Change errors to be catchable exceptions, the exception contains both
a msg
and debug_info we expose via the api call
- Update the api call to provide a summary error_messages (for Marco's
needs)
but keep the details in the actual list of errors with the messages and
the
debug info we have that went into those errors.

Sample proof api call and response:
http://paste.mitechie.com/show/1049/

Note: this is only adding the new proof checks of validating the
config/options from the charm found in the database to the options
defined in
the bundle.

- Validate the option exists in the charm we found in the db
- Validate that the type is of the correct type.
- Validate that the charm we found has options at all.

QA
---
You have to ingest the charms so that we can validate them. You can then
toss
the yaml file (as a single string) to the proof api endpoint as a POST'd
key
deployer_file. Right now it only handles yaml.

R=
CC=
https://codereview.appspot.com/14789043

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py
File charmworld/lib/proof.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode4
charmworld/lib/proof.py:4: from charmworld.models import (
I'm not a fan of importing models into a lib file, open to ideas of ways
to make this better other than passing in a resolver and a descriptor
into the proof calls from the view.

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode54
charmworld/lib/proof.py:54: if not charm.options:
this is only called if it's expecting to proof options. So not having
any is an error. (test_key and test_value are required inputs)

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode67
charmworld/lib/proof.py:67: # # The config key is a valid one for this
charm. Check that the value
doh, will clean up.

Description:
Add charm config proofing and update api response

- Move proof logic into a new lib/proof.py module
- Update the proof api call to look through the bundles and proof them
- Change errors to be catchable exceptions, the exception contains both
a msg
and debug_info we expose via the api call
- Update the api call to provide a summary error_messages (for Marco's
needs)
but keep the details in the actual list of errors with the messages and
the
debug info we have that went into those errors.

Sample proof api call and response:
http://paste.mitechie.com/show/1049/

Note: this is only adding the new proof checks of validating the
config/options from the charm found in the database to the options
defined in
the bundle.

- Validate the option exists in the charm we found in the db
- Validate that the type is of the correct type.
- Validate that the charm we found has options at all.

QA
---
You have to ingest the charms so that we can validate them. You can then
toss
the yaml file (as a single string) to the proof api endpoint as a POST'd
key
deploye...

Read more...

Revision history for this message
Richard Harding (rharding) wrote :

Reviewer comments added.

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py
File charmworld/views/api.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py#newcode501
charmworld/views/api.py:501: 'errors': [],
this is the list of actual errors. They can be global messages ('could
not parse the file') or objects from error checking a specific bundle in
the file.

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py#newcode502
charmworld/views/api.py:502: 'error_messages': [],
this is a summary of all error messages provided as an aid to marco.

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py#newcode503
charmworld/views/api.py:503: 'debug_info': [],
and a global level 'debug_info' to match the representation of errors of
other areas of the code where they all provide a message as well as
helpful debug information to assist in finding the way to correct the
error.

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py#newcode514
charmworld/views/api.py:514: try:
moved the old checks to the try:catch for the ProofError. I felt this
read a bit cleaner and allows for a function to return a value (the
parsed yaml file) normally, but have a complex object come back during a
failure.

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/tests/test_api.py
File charmworld/views/tests/test_api.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/tests/test_api.py#newcode905
charmworld/views/tests/test_api.py:905:
results['errors'][0]['services']['db'][0]['message'])
this is due to the move to support multiple errors on the same service
within a bundle.

https://codereview.appspot.com/14789043/

Revision history for this message
Gary Poster (gary) wrote :
Download full text (6.2 KiB)

Hi Rick. Thank you for a nice branch. LGTM.

I give a ton of comments that could lead to a decent amount of work.
Please treat all of them as suggestions, though I do think that the code
in the BundleProof code ought to move back to the view, somehow, and I
hope I made at least a somewhat compelling case for it.

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py
File charmworld/lib/proof.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode4
charmworld/lib/proof.py:4: from charmworld.models import (
On 2013/10/18 19:09:24, rharding wrote:
> I'm not a fan of importing models into a lib file, open to ideas of
ways to make
> this better other than passing in a resolver and a descriptor into the
proof
> calls from the view.

Similar alternative: You have to instantiate BundleProof with these
extra bits, to create something that the view can use? Don't know
charmworld, so I don't know where it would be stashed, but that broad
approach seems tried and true.

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode20
charmworld/lib/proof.py:20: def check_parseable_deployer_file(data,
format='yaml'):
Ah, ye olde naming problem: a validation function also does work. :-)
If you really cared about the naming problem, I would suggest naming
this "parse_deployer_file". The fact that you convert the error (while
tossing out any helpful information from the parser's error, btw :-/)
seems minor compared to the fact that this parses the data. <shrug>
(which means "do as you will")

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode41
charmworld/lib/proof.py:41: return charm_found
Uh oh, this validation function is doing work too. :-/

Um.

This doesn't feel right. The Bundle proof bits here feel like they
belong in an integration layer, like a view, rather than in a library.
Do with that what you will.

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/proof.py#newcode54
charmworld/lib/proof.py:54: if not charm.options:
On 2013/10/18 19:09:24, rharding wrote:
> this is only called if it's expecting to proof options. So not having
any is an
> error. (test_key and test_value are required inputs)

Is this an error of the bundle author or in the charmworld code? It
feels like the latter, in which case raising a ProofError seems wrong.
We ought to log something instead. If I'm wrong, nevermind.

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/tests/test_proof.py
File charmworld/lib/tests/test_proof.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/tests/test_proof.py#newcode19
charmworld/lib/tests/test_proof.py:19: sample_deployer_file = """
...This is JSON...

https://codereview.appspot.com/14789043/diff/3001/charmworld/lib/tests/test_proof.py#newcode33
charmworld/lib/tests/test_proof.py:33: """A deployer file should be
parseable as yaml."""
"...as YAML or JSON."?

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py
File charmworld/views/api.py (right):

https://codereview.appspot.com/14789043/diff/3001/charmworld/views/api.py#newcode497
charmworld/views...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'charmworld/lib/proof.py'
--- charmworld/lib/proof.py 1970-01-01 00:00:00 +0000
+++ charmworld/lib/proof.py 2013-10-18 18:57:29 +0000
@@ -0,0 +1,76 @@
1"""Helpers to proof charms and bundles."""
2import yaml
3
4from charmworld.models import (
5 BundledCharmDescription,
6 resolve_charm_from_description,
7)
8
9
10class ProofError(Exception):
11
12 def __init__(self, debug_info, msg):
13 self.debug_info = debug_info
14 self.msg = msg
15
16
17class BundleProof(object):
18
19 @staticmethod
20 def check_parseable_deployer_file(data, format='yaml'):
21 try:
22 bundle_data = yaml.safe_load(data)
23 except yaml.YAMLError:
24 raise ProofError(
25 data,
26 'Could not parse the yaml provided.'
27 )
28 return bundle_data
29
30 @staticmethod
31 def check_service_exists(db, name, service_data, bundle_config):
32 charm_description = BundledCharmDescription(
33 'proof', service_data, bundle_config.get('series'))
34 charm_found = resolve_charm_from_description(db, charm_description)
35 if not charm_found:
36 raise ProofError(
37 charm_description.get_process(),
38 'Could not find charm: ' + name,
39 )
40 else:
41 return charm_found
42
43
44class CharmProof(object):
45
46 @staticmethod
47 def check_config(charm, test_key, test_value):
48 type_map = {
49 'boolean': ('bool',),
50 'int': ('int',),
51 'string': ('str', 'unicode'),
52 }
53
54 if not charm.options:
55 raise ProofError([
56 'Looking at charm id: ' + charm._id],
57 'The charm has no options.'
58 )
59
60 if test_key not in charm.options:
61 raise ProofError([
62 'Looking at charm id: ' + charm._id,
63 'Found config keys: ' + str(charm.options.keys())],
64 'The charm has no option for: ' + test_key
65 )
66
67 # # The config key is a valid one for this charm. Check that the value
68 # # is of the right type.
69 specified_type = charm.options[test_key]['type']
70 valid_types = type_map[specified_type]
71 msg = "%s is not of type %s."
72 if type(test_value) not in valid_types:
73 raise ProofError(
74 ['Looking at charm id: ' + charm._id],
75 msg % (test_key, specified_type)
76 )
077
=== added directory 'charmworld/lib/tests'
=== added file 'charmworld/lib/tests/__init__.py'
=== added file 'charmworld/lib/tests/test_proof.py'
--- charmworld/lib/tests/test_proof.py 1970-01-01 00:00:00 +0000
+++ charmworld/lib/tests/test_proof.py 2013-10-18 18:57:29 +0000
@@ -0,0 +1,151 @@
1from mock import (
2 Mock,
3 patch,
4)
5import yaml
6
7from charmworld.lib.proof import (
8 BundleProof,
9 CharmProof,
10 ProofError,
11)
12from charmworld.models import Charm
13from charmworld.testing import TestCase
14
15
16class TestBundleProof(TestCase):
17 """Verify that a bundle can be proofed in charmworld."""
18
19 sample_deployer_file = """
20 {
21 "wiki": {
22 "services": {
23 "wiki": {
24 "charm": "mediawiki",
25 "num_units": 2,
26 "branch": "lp:charms/precise/mediawiki",
27 "constraints": "mem=2"
28 }
29 },
30 "series": "precise" }}"""
31
32 def test_check_parseable_yaml(self):
33 """A deployer file should be parseable as yaml."""
34
35 deployer_file = self.sample_deployer_file
36 result = BundleProof.check_parseable_deployer_file(deployer_file)
37 self.assertEqual(result['wiki']['services'].keys(), ['wiki'])
38
39 def test_failing_parsing_yaml_throws_proof_error(self):
40 """If the yaml is not parse the yaml thows a ProofError"""
41 deployer_file = '{'
42 with self.assertRaises(ProofError) as exc:
43 BundleProof.check_parseable_deployer_file(deployer_file)
44
45 self.assertEqual(
46 'Could not parse the yaml provided.',
47 exc.exception.msg
48 )
49 self.assertEqual(
50 deployer_file,
51 exc.exception.debug_info
52 )
53
54 @patch('charmworld.lib.proof.resolve_charm_from_description')
55 def test_service_not_found_raises_proof_error(self, resolver):
56 """If we cannot find a service specified it's a ProofError"""
57 resolver.return_value = None
58 deployer = yaml.load(self.sample_deployer_file)
59
60 with self.assertRaises(ProofError) as exc:
61 BundleProof.check_service_exists(
62 None,
63 'wiki',
64 deployer['wiki']['services']['wiki'],
65 deployer['wiki'])
66
67 self.assertEqual(
68 "Could not find charm: wiki",
69 exc.exception.msg
70 )
71 # We should have an array of logic behind the decision to look up the
72 # charm. Just check we've got some logic. This number might get
73 # tweaked, but we're just checking it made it to the exception, not
74 # what the reasons are.
75 self.assertEqual(
76 4,
77 len(exc.exception.debug_info)
78 )
79
80
81class TestCharmProof(TestCase):
82
83 def make_fake_charm_config(self, options):
84 charm = Charm({
85 '_id': '/precise/id',
86 'config': options
87 })
88
89 return charm
90
91 def test_check_config_fails_when_no_options_defined(self):
92 """Bundles might attempt to set config values that are not valid"""
93 charm = Mock()
94 charm.options = {}
95 charm._id = 'precise/test'
96
97 with self.assertRaises(ProofError) as exc:
98 CharmProof.check_config(charm, 'name', 'test')
99
100 self.assertEqual(
101 'The charm has no options.',
102 exc.exception.msg
103 )
104 self.assertEqual(
105 ['Looking at charm id: precise/test'],
106 exc.exception.debug_info
107 )
108
109 def test_check_config_bool(self):
110 """Bundles might attempt to set a boolean config value string/int"""
111 charm = Mock()
112 charm.options = {
113 'name': {
114 'type': 'boolean',
115 'default': True,
116 }
117 }
118 charm._id = 'precise/test'
119
120 with self.assertRaises(ProofError) as exc:
121 CharmProof.check_config(charm, 'name', 'test')
122
123 self.assertEqual(
124 'name is not of type boolean.',
125 exc.exception.msg
126 )
127 self.assertEqual(
128 ['Looking at charm id: precise/test'],
129 exc.exception.debug_info
130 )
131
132 def test_check_config_fails_when_option_does_not_exist(self):
133 """Bundles might attempt to set config values that are not valid"""
134 charm = Mock()
135 charm.options = {'foo': {}}
136 charm._id = 'precise/test'
137
138 with self.assertRaises(ProofError) as exc:
139 CharmProof.check_config(charm, 'name', 'test')
140
141 self.assertEqual(
142 'The charm has no option for: name',
143 exc.exception.msg
144 )
145 self.assertEqual(
146 [
147 'Looking at charm id: precise/test',
148 'Found config keys: [\'foo\']'
149 ],
150 exc.exception.debug_info
151 )
0152
=== modified file 'charmworld/views/api.py'
--- charmworld/views/api.py 2013-10-15 16:18:49 +0000
+++ charmworld/views/api.py 2013-10-18 18:57:29 +0000
@@ -12,7 +12,6 @@
12)12)
13import re13import re
14from urllib import unquote14from urllib import unquote
15import yaml
1615
17from gridfs.errors import NoFile16from gridfs.errors import NoFile
18import pymongo17import pymongo
@@ -23,6 +22,11 @@
23from pyramid.view import view_config22from pyramid.view import view_config
24from webob import Response23from webob import Response
2524
25from charmworld.lib.proof import (
26 BundleProof,
27 CharmProof,
28 ProofError,
29)
26from charmworld.models import (30from charmworld.models import (
27 Bundle,31 Bundle,
28 BundledCharmDescription,32 BundledCharmDescription,
@@ -495,50 +499,94 @@
495 # The top level is for general deployer file errors.499 # The top level is for general deployer file errors.
496 response_data = {500 response_data = {
497 'errors': [],501 'errors': [],
502 'error_messages': [],
503 'debug_info': [],
498 }504 }
499 bundle_string = request.params.get('deployer_file')505 bundle_string = request.params.get('deployer_file')
500 bundle_format = request.params.get('bundle_format', 'yaml')506 bundle_format = request.params.get('bundle_format', 'yaml')
501 bundle_data = None507 bundle_data = None
502508
503 if not bundle_string:509 if not bundle_string:
504 response_data['errors'].append('No deployer file data received.')510 response_data['error_messages'].append(
505 return json_response(400, response_data)511 'No deployer file data received.')
506512 return json_response(400, response_data)
507 if bundle_format == 'yaml':513
508 try:514 try:
509 bundle_data = yaml.safe_load(bundle_string)515 bundle_data = BundleProof.check_parseable_deployer_file(
510 except yaml.YAMLError:516 bundle_string,
511 response_data['errors'].append(517 format=bundle_format)
512 'Could not parse the yaml provided.')518 except ProofError, exc:
513 return json_response(400, response_data)519 # If we cannot parse the config file there's nothing more to do.
514520 # Return immediately.
515 # Proof each bundle in the deployer file.521 response_data['error_messages'].append(exc.msg)
516 for bundle_name, bundle_config in bundle_data.items():522 response_data['debug_info'].append(exc.debug_info)
517 has_bundle_error = False523 return json_response(400, response_data)
518 bundle_info = {524
519 'name': bundle_name,525 # Proof each bundle in the deployer file.
520 'services': {},526 for bundle_name, bundle_config in bundle_data.items():
521 'relations': {},527 bundle_info = {
522 }528 'name': bundle_name,
523529 'services': {},
524 for name, charm in bundle_config.get('services').items():530 'relations': {},
525 charm_description = BundledCharmDescription(531 }
526 'proof', charm, bundle_config.get('series'))532 found_charms = {}
527533
528 if not resolve_charm_from_description(534 # Verify we can find the charms at all.
529 self.request.db, charm_description):535 for name, charm in bundle_config.get('services').items():
530 has_bundle_error = True536 try:
531 # The error is keyed to the charm and includes a537 charm_found = BundleProof.check_service_exists(
532 # general message + the description's thought process.538 self.request.db, name, charm, bundle_config)
533 bundle_info['services'][name] = {539 found_charms[name] = Charm(charm_found)
534 'message': 'Could not find charm: ' + name,540 except ProofError, exc:
535 'process': charm_description.get_process()541 # We could not find the charm. No other verification can
536 }542 # happen.
537543 bundle_info['services'][name] = []
538 if has_bundle_error:544 bundle_info['services'][name].append({
539 response_data['errors'].append(bundle_info)545 'message': exc.msg,
540546 'debug_info': exc.debug_info,
541 return json_response(200, response_data)547 })
548
549 # For the charms we did find in the system, verify that their
550 # config is valid for the values allowed by the charm we found.
551 for name, charm in found_charms.iteritems():
552 check_config = bundle_config['services'][name].get('options')
553 if not check_config:
554 # There's no config specified to validate.
555 continue
556 for test_key, test_value in check_config.iteritems():
557 try:
558 CharmProof.check_config(charm, test_key, test_value)
559 except ProofError, exc:
560 if name not in bundle_info['services']:
561 bundle_info['services'][name] = []
562
563 bundle_info['services'][name].append({
564 'message': exc.msg,
565 'debug_info': exc.debug_info,
566 })
567
568 # If there are errors in this bundles services, make sure we
569 # append the info to the response data that gets sent back to the
570 # user.
571 if bundle_info['services'].keys():
572 response_data['errors'].append(bundle_info)
573
574 # Build up a root messages list of errors that can be used to help the
575 # client go through them all easily.
576 error_messages = []
577 if response_data['errors']:
578 for bundle in response_data['errors']:
579 prefix = bundle['name']
580 for service_name, service in bundle['services'].items():
581 for err in service:
582 msg = '%s: %s' % (prefix, err['message'])
583 error_messages.append(msg)
584 if error_messages:
585 response_data['error_messages'].extend(error_messages)
586
587 # After proofing each deployer file return with any failure info
588 # found.
589 return json_response(200, response_data)
542590
543 def charm(self, path=None):591 def charm(self, path=None):
544 """Retrieve a charm according to its API ID (the path prefix)."""592 """Retrieve a charm according to its API ID (the path prefix)."""
545593
=== modified file 'charmworld/views/tests/test_api.py'
--- charmworld/views/tests/test_api.py 2013-10-15 16:18:49 +0000
+++ charmworld/views/tests/test_api.py 2013-10-18 18:57:29 +0000
@@ -874,7 +874,7 @@
874 self.assertEqual(400, response.status_code)874 self.assertEqual(400, response.status_code)
875 self.assertEqual(875 self.assertEqual(
876 'No deployer file data received.',876 'No deployer file data received.',
877 response.json_body['errors'][0])877 response.json_body['error_messages'][0])
878878
879 def test_bundle_proof_invalid_deployer_file(self):879 def test_bundle_proof_invalid_deployer_file(self):
880 request = self.make_request('bundle', remainder='/proof')880 request = self.make_request('bundle', remainder='/proof')
@@ -885,7 +885,7 @@
885 self.assertEqual(400, response.status_code)885 self.assertEqual(400, response.status_code)
886 self.assertIn(886 self.assertIn(
887 'Could not parse',887 'Could not parse',
888 response.json_body['errors'][0])888 response.json_body['error_messages'][0])
889889
890 def test_bundle_proof_charm_exists(self):890 def test_bundle_proof_charm_exists(self):
891 deployer_file = load_data_file('sample_bundle/bundles.yaml')891 deployer_file = load_data_file('sample_bundle/bundles.yaml')
@@ -902,12 +902,12 @@
902 self.assertEqual(4, len(results['errors'][0]['services'].keys()))902 self.assertEqual(4, len(results['errors'][0]['services'].keys()))
903 self.assertIn(903 self.assertIn(
904 'Could not find ',904 'Could not find ',
905 results['errors'][0]['services']['db']['message'])905 results['errors'][0]['services']['db'][0]['message'])
906906
907 # And each failure has a list of 'process' information.907 # And each failure has a list of 'process' information.
908 self.assertIn(908 self.assertIn(
909 'process',909 'debug_info',
910 results['errors'][0]['services']['db'].keys())910 results['errors'][0]['services']['db'][0].keys())
911911
912 def test_bundle_proof_valid(self):912 def test_bundle_proof_valid(self):
913 # We have to create the charm so that we can verify it exists.913 # We have to create the charm so that we can verify it exists.
@@ -939,6 +939,99 @@
939 results = response.json_body939 results = response.json_body
940 self.assertEqual(0, len(results['errors']))940 self.assertEqual(0, len(results['errors']))
941941
942 def test_bundle_proof_invalid_config_type(self):
943 # We have to create the charm so that we can verify it exists.
944 _id, charm = factory.makeCharm(
945 self.db,
946 description=''
947 )
948
949 # and we'll cheat and just find it straight by the store url.
950 charm = Charm(charm)
951 store_url = charm.store_url
952
953 bundle_config = {
954 'wiki': {
955 'services': {
956 'charm': {
957 'charm_url': str(store_url),
958 'options': {
959 'script-interval': 'invalid'
960 }
961 }
962 }
963 }
964 }
965
966 request = self.make_request('bundle', remainder='/proof')
967 request.params = {
968 'deployer_file': yaml.dump(bundle_config)
969 }
970 response = self.api_class(request)()
971 self.assertEqual(200, response.status_code)
972 results = response.json_body
973 self.assertEqual(1, len(results['errors']))
974 self.assertEqual(1, len(results['error_messages']))
975
976 # Note that the View will prefix the error message from the proof with
977 # the bundle name to help identify where the error message came from.
978 self.assertEqual(
979 'wiki: script-interval is not of type int.',
980 results['error_messages'][0]
981 )
982
983 def test_bundle_proof_supports_multiple_errors(self):
984 _id, charm = factory.makeCharm(
985 self.db,
986 description=''
987 )
988
989 # and we'll cheat and just find it straight by the store url.
990 charm = Charm(charm)
991 store_url = charm.store_url
992
993 bundle_config = {
994 'wiki': {
995 'services': {
996 'charm': {
997 'charm_url': str(store_url),
998 'options': {
999 'script-interval': 'invalid',
1000 'no-exist': 'hah invalid',
1001 }
1002 },
1003 'fail': {
1004 'name': 'will-fail'
1005 }
1006 }
1007 }
1008 }
1009
1010 request = self.make_request('bundle', remainder='/proof')
1011 request.params = {
1012 'deployer_file': yaml.dump(bundle_config)
1013 }
1014 response = self.api_class(request)()
1015 self.assertEqual(200, response.status_code)
1016 results = response.json_body
1017 self.assertEqual(1, len(results['errors']))
1018 self.assertEqual(3, len(results['error_messages']))
1019
1020 # One message from not finding a charm.
1021 self.assertEqual(
1022 'wiki: Could not find charm: fail',
1023 results['error_messages'][0]
1024 )
1025 # The other two from checking the config of the other charm.
1026 self.assertEqual(
1027 'wiki: script-interval is not of type int.',
1028 results['error_messages'][1]
1029 )
1030 self.assertEqual(
1031 'wiki: The charm has no option for: no-exist',
1032 results['error_messages'][2]
1033 )
1034
9421035
943class TestAPI3Bundles(TestAPIBundles, API3Mixin):1036class TestAPI3Bundles(TestAPIBundles, API3Mixin):
944 """Test API 3 bundle endpoint."""1037 """Test API 3 bundle endpoint."""

Subscribers

People subscribed via source and target branches