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 on 2013-10-17

Fix for the updated error_messages

433. By Richard Harding on 2013-10-18

add bundle proof tests

434. By Richard Harding on 2013-10-18

Add proof tests for the CharmProof

435. By Richard Harding on 2013-10-18

Add support for checking the updated proofing call

436. By Richard Harding on 2013-10-18

Update test to be a bit better demo of multiple errors

437. By Richard Harding on 2013-10-18

lint

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...

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/

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
1=== added file 'charmworld/lib/proof.py'
2--- charmworld/lib/proof.py 1970-01-01 00:00:00 +0000
3+++ charmworld/lib/proof.py 2013-10-18 18:57:29 +0000
4@@ -0,0 +1,76 @@
5+"""Helpers to proof charms and bundles."""
6+import yaml
7+
8+from charmworld.models import (
9+ BundledCharmDescription,
10+ resolve_charm_from_description,
11+)
12+
13+
14+class ProofError(Exception):
15+
16+ def __init__(self, debug_info, msg):
17+ self.debug_info = debug_info
18+ self.msg = msg
19+
20+
21+class BundleProof(object):
22+
23+ @staticmethod
24+ def check_parseable_deployer_file(data, format='yaml'):
25+ try:
26+ bundle_data = yaml.safe_load(data)
27+ except yaml.YAMLError:
28+ raise ProofError(
29+ data,
30+ 'Could not parse the yaml provided.'
31+ )
32+ return bundle_data
33+
34+ @staticmethod
35+ def check_service_exists(db, name, service_data, bundle_config):
36+ charm_description = BundledCharmDescription(
37+ 'proof', service_data, bundle_config.get('series'))
38+ charm_found = resolve_charm_from_description(db, charm_description)
39+ if not charm_found:
40+ raise ProofError(
41+ charm_description.get_process(),
42+ 'Could not find charm: ' + name,
43+ )
44+ else:
45+ return charm_found
46+
47+
48+class CharmProof(object):
49+
50+ @staticmethod
51+ def check_config(charm, test_key, test_value):
52+ type_map = {
53+ 'boolean': ('bool',),
54+ 'int': ('int',),
55+ 'string': ('str', 'unicode'),
56+ }
57+
58+ if not charm.options:
59+ raise ProofError([
60+ 'Looking at charm id: ' + charm._id],
61+ 'The charm has no options.'
62+ )
63+
64+ if test_key not in charm.options:
65+ raise ProofError([
66+ 'Looking at charm id: ' + charm._id,
67+ 'Found config keys: ' + str(charm.options.keys())],
68+ 'The charm has no option for: ' + test_key
69+ )
70+
71+ # # The config key is a valid one for this charm. Check that the value
72+ # # is of the right type.
73+ specified_type = charm.options[test_key]['type']
74+ valid_types = type_map[specified_type]
75+ msg = "%s is not of type %s."
76+ if type(test_value) not in valid_types:
77+ raise ProofError(
78+ ['Looking at charm id: ' + charm._id],
79+ msg % (test_key, specified_type)
80+ )
81
82=== added directory 'charmworld/lib/tests'
83=== added file 'charmworld/lib/tests/__init__.py'
84=== added file 'charmworld/lib/tests/test_proof.py'
85--- charmworld/lib/tests/test_proof.py 1970-01-01 00:00:00 +0000
86+++ charmworld/lib/tests/test_proof.py 2013-10-18 18:57:29 +0000
87@@ -0,0 +1,151 @@
88+from mock import (
89+ Mock,
90+ patch,
91+)
92+import yaml
93+
94+from charmworld.lib.proof import (
95+ BundleProof,
96+ CharmProof,
97+ ProofError,
98+)
99+from charmworld.models import Charm
100+from charmworld.testing import TestCase
101+
102+
103+class TestBundleProof(TestCase):
104+ """Verify that a bundle can be proofed in charmworld."""
105+
106+ sample_deployer_file = """
107+ {
108+ "wiki": {
109+ "services": {
110+ "wiki": {
111+ "charm": "mediawiki",
112+ "num_units": 2,
113+ "branch": "lp:charms/precise/mediawiki",
114+ "constraints": "mem=2"
115+ }
116+ },
117+ "series": "precise" }}"""
118+
119+ def test_check_parseable_yaml(self):
120+ """A deployer file should be parseable as yaml."""
121+
122+ deployer_file = self.sample_deployer_file
123+ result = BundleProof.check_parseable_deployer_file(deployer_file)
124+ self.assertEqual(result['wiki']['services'].keys(), ['wiki'])
125+
126+ def test_failing_parsing_yaml_throws_proof_error(self):
127+ """If the yaml is not parse the yaml thows a ProofError"""
128+ deployer_file = '{'
129+ with self.assertRaises(ProofError) as exc:
130+ BundleProof.check_parseable_deployer_file(deployer_file)
131+
132+ self.assertEqual(
133+ 'Could not parse the yaml provided.',
134+ exc.exception.msg
135+ )
136+ self.assertEqual(
137+ deployer_file,
138+ exc.exception.debug_info
139+ )
140+
141+ @patch('charmworld.lib.proof.resolve_charm_from_description')
142+ def test_service_not_found_raises_proof_error(self, resolver):
143+ """If we cannot find a service specified it's a ProofError"""
144+ resolver.return_value = None
145+ deployer = yaml.load(self.sample_deployer_file)
146+
147+ with self.assertRaises(ProofError) as exc:
148+ BundleProof.check_service_exists(
149+ None,
150+ 'wiki',
151+ deployer['wiki']['services']['wiki'],
152+ deployer['wiki'])
153+
154+ self.assertEqual(
155+ "Could not find charm: wiki",
156+ exc.exception.msg
157+ )
158+ # We should have an array of logic behind the decision to look up the
159+ # charm. Just check we've got some logic. This number might get
160+ # tweaked, but we're just checking it made it to the exception, not
161+ # what the reasons are.
162+ self.assertEqual(
163+ 4,
164+ len(exc.exception.debug_info)
165+ )
166+
167+
168+class TestCharmProof(TestCase):
169+
170+ def make_fake_charm_config(self, options):
171+ charm = Charm({
172+ '_id': '/precise/id',
173+ 'config': options
174+ })
175+
176+ return charm
177+
178+ def test_check_config_fails_when_no_options_defined(self):
179+ """Bundles might attempt to set config values that are not valid"""
180+ charm = Mock()
181+ charm.options = {}
182+ charm._id = 'precise/test'
183+
184+ with self.assertRaises(ProofError) as exc:
185+ CharmProof.check_config(charm, 'name', 'test')
186+
187+ self.assertEqual(
188+ 'The charm has no options.',
189+ exc.exception.msg
190+ )
191+ self.assertEqual(
192+ ['Looking at charm id: precise/test'],
193+ exc.exception.debug_info
194+ )
195+
196+ def test_check_config_bool(self):
197+ """Bundles might attempt to set a boolean config value string/int"""
198+ charm = Mock()
199+ charm.options = {
200+ 'name': {
201+ 'type': 'boolean',
202+ 'default': True,
203+ }
204+ }
205+ charm._id = 'precise/test'
206+
207+ with self.assertRaises(ProofError) as exc:
208+ CharmProof.check_config(charm, 'name', 'test')
209+
210+ self.assertEqual(
211+ 'name is not of type boolean.',
212+ exc.exception.msg
213+ )
214+ self.assertEqual(
215+ ['Looking at charm id: precise/test'],
216+ exc.exception.debug_info
217+ )
218+
219+ def test_check_config_fails_when_option_does_not_exist(self):
220+ """Bundles might attempt to set config values that are not valid"""
221+ charm = Mock()
222+ charm.options = {'foo': {}}
223+ charm._id = 'precise/test'
224+
225+ with self.assertRaises(ProofError) as exc:
226+ CharmProof.check_config(charm, 'name', 'test')
227+
228+ self.assertEqual(
229+ 'The charm has no option for: name',
230+ exc.exception.msg
231+ )
232+ self.assertEqual(
233+ [
234+ 'Looking at charm id: precise/test',
235+ 'Found config keys: [\'foo\']'
236+ ],
237+ exc.exception.debug_info
238+ )
239
240=== modified file 'charmworld/views/api.py'
241--- charmworld/views/api.py 2013-10-15 16:18:49 +0000
242+++ charmworld/views/api.py 2013-10-18 18:57:29 +0000
243@@ -12,7 +12,6 @@
244 )
245 import re
246 from urllib import unquote
247-import yaml
248
249 from gridfs.errors import NoFile
250 import pymongo
251@@ -23,6 +22,11 @@
252 from pyramid.view import view_config
253 from webob import Response
254
255+from charmworld.lib.proof import (
256+ BundleProof,
257+ CharmProof,
258+ ProofError,
259+)
260 from charmworld.models import (
261 Bundle,
262 BundledCharmDescription,
263@@ -495,50 +499,94 @@
264 # The top level is for general deployer file errors.
265 response_data = {
266 'errors': [],
267+ 'error_messages': [],
268+ 'debug_info': [],
269 }
270 bundle_string = request.params.get('deployer_file')
271 bundle_format = request.params.get('bundle_format', 'yaml')
272 bundle_data = None
273
274 if not bundle_string:
275- response_data['errors'].append('No deployer file data received.')
276- return json_response(400, response_data)
277-
278- if bundle_format == 'yaml':
279- try:
280- bundle_data = yaml.safe_load(bundle_string)
281- except yaml.YAMLError:
282- response_data['errors'].append(
283- 'Could not parse the yaml provided.')
284- return json_response(400, response_data)
285-
286- # Proof each bundle in the deployer file.
287- for bundle_name, bundle_config in bundle_data.items():
288- has_bundle_error = False
289- bundle_info = {
290- 'name': bundle_name,
291- 'services': {},
292- 'relations': {},
293- }
294-
295- for name, charm in bundle_config.get('services').items():
296- charm_description = BundledCharmDescription(
297- 'proof', charm, bundle_config.get('series'))
298-
299- if not resolve_charm_from_description(
300- self.request.db, charm_description):
301- has_bundle_error = True
302- # The error is keyed to the charm and includes a
303- # general message + the description's thought process.
304- bundle_info['services'][name] = {
305- 'message': 'Could not find charm: ' + name,
306- 'process': charm_description.get_process()
307- }
308-
309- if has_bundle_error:
310- response_data['errors'].append(bundle_info)
311-
312- return json_response(200, response_data)
313+ response_data['error_messages'].append(
314+ 'No deployer file data received.')
315+ return json_response(400, response_data)
316+
317+ try:
318+ bundle_data = BundleProof.check_parseable_deployer_file(
319+ bundle_string,
320+ format=bundle_format)
321+ except ProofError, exc:
322+ # If we cannot parse the config file there's nothing more to do.
323+ # Return immediately.
324+ response_data['error_messages'].append(exc.msg)
325+ response_data['debug_info'].append(exc.debug_info)
326+ return json_response(400, response_data)
327+
328+ # Proof each bundle in the deployer file.
329+ for bundle_name, bundle_config in bundle_data.items():
330+ bundle_info = {
331+ 'name': bundle_name,
332+ 'services': {},
333+ 'relations': {},
334+ }
335+ found_charms = {}
336+
337+ # Verify we can find the charms at all.
338+ for name, charm in bundle_config.get('services').items():
339+ try:
340+ charm_found = BundleProof.check_service_exists(
341+ self.request.db, name, charm, bundle_config)
342+ found_charms[name] = Charm(charm_found)
343+ except ProofError, exc:
344+ # We could not find the charm. No other verification can
345+ # happen.
346+ bundle_info['services'][name] = []
347+ bundle_info['services'][name].append({
348+ 'message': exc.msg,
349+ 'debug_info': exc.debug_info,
350+ })
351+
352+ # For the charms we did find in the system, verify that their
353+ # config is valid for the values allowed by the charm we found.
354+ for name, charm in found_charms.iteritems():
355+ check_config = bundle_config['services'][name].get('options')
356+ if not check_config:
357+ # There's no config specified to validate.
358+ continue
359+ for test_key, test_value in check_config.iteritems():
360+ try:
361+ CharmProof.check_config(charm, test_key, test_value)
362+ except ProofError, exc:
363+ if name not in bundle_info['services']:
364+ bundle_info['services'][name] = []
365+
366+ bundle_info['services'][name].append({
367+ 'message': exc.msg,
368+ 'debug_info': exc.debug_info,
369+ })
370+
371+ # If there are errors in this bundles services, make sure we
372+ # append the info to the response data that gets sent back to the
373+ # user.
374+ if bundle_info['services'].keys():
375+ response_data['errors'].append(bundle_info)
376+
377+ # Build up a root messages list of errors that can be used to help the
378+ # client go through them all easily.
379+ error_messages = []
380+ if response_data['errors']:
381+ for bundle in response_data['errors']:
382+ prefix = bundle['name']
383+ for service_name, service in bundle['services'].items():
384+ for err in service:
385+ msg = '%s: %s' % (prefix, err['message'])
386+ error_messages.append(msg)
387+ if error_messages:
388+ response_data['error_messages'].extend(error_messages)
389+
390+ # After proofing each deployer file return with any failure info
391+ # found.
392+ return json_response(200, response_data)
393
394 def charm(self, path=None):
395 """Retrieve a charm according to its API ID (the path prefix)."""
396
397=== modified file 'charmworld/views/tests/test_api.py'
398--- charmworld/views/tests/test_api.py 2013-10-15 16:18:49 +0000
399+++ charmworld/views/tests/test_api.py 2013-10-18 18:57:29 +0000
400@@ -874,7 +874,7 @@
401 self.assertEqual(400, response.status_code)
402 self.assertEqual(
403 'No deployer file data received.',
404- response.json_body['errors'][0])
405+ response.json_body['error_messages'][0])
406
407 def test_bundle_proof_invalid_deployer_file(self):
408 request = self.make_request('bundle', remainder='/proof')
409@@ -885,7 +885,7 @@
410 self.assertEqual(400, response.status_code)
411 self.assertIn(
412 'Could not parse',
413- response.json_body['errors'][0])
414+ response.json_body['error_messages'][0])
415
416 def test_bundle_proof_charm_exists(self):
417 deployer_file = load_data_file('sample_bundle/bundles.yaml')
418@@ -902,12 +902,12 @@
419 self.assertEqual(4, len(results['errors'][0]['services'].keys()))
420 self.assertIn(
421 'Could not find ',
422- results['errors'][0]['services']['db']['message'])
423+ results['errors'][0]['services']['db'][0]['message'])
424
425 # And each failure has a list of 'process' information.
426 self.assertIn(
427- 'process',
428- results['errors'][0]['services']['db'].keys())
429+ 'debug_info',
430+ results['errors'][0]['services']['db'][0].keys())
431
432 def test_bundle_proof_valid(self):
433 # We have to create the charm so that we can verify it exists.
434@@ -939,6 +939,99 @@
435 results = response.json_body
436 self.assertEqual(0, len(results['errors']))
437
438+ def test_bundle_proof_invalid_config_type(self):
439+ # We have to create the charm so that we can verify it exists.
440+ _id, charm = factory.makeCharm(
441+ self.db,
442+ description=''
443+ )
444+
445+ # and we'll cheat and just find it straight by the store url.
446+ charm = Charm(charm)
447+ store_url = charm.store_url
448+
449+ bundle_config = {
450+ 'wiki': {
451+ 'services': {
452+ 'charm': {
453+ 'charm_url': str(store_url),
454+ 'options': {
455+ 'script-interval': 'invalid'
456+ }
457+ }
458+ }
459+ }
460+ }
461+
462+ request = self.make_request('bundle', remainder='/proof')
463+ request.params = {
464+ 'deployer_file': yaml.dump(bundle_config)
465+ }
466+ response = self.api_class(request)()
467+ self.assertEqual(200, response.status_code)
468+ results = response.json_body
469+ self.assertEqual(1, len(results['errors']))
470+ self.assertEqual(1, len(results['error_messages']))
471+
472+ # Note that the View will prefix the error message from the proof with
473+ # the bundle name to help identify where the error message came from.
474+ self.assertEqual(
475+ 'wiki: script-interval is not of type int.',
476+ results['error_messages'][0]
477+ )
478+
479+ def test_bundle_proof_supports_multiple_errors(self):
480+ _id, charm = factory.makeCharm(
481+ self.db,
482+ description=''
483+ )
484+
485+ # and we'll cheat and just find it straight by the store url.
486+ charm = Charm(charm)
487+ store_url = charm.store_url
488+
489+ bundle_config = {
490+ 'wiki': {
491+ 'services': {
492+ 'charm': {
493+ 'charm_url': str(store_url),
494+ 'options': {
495+ 'script-interval': 'invalid',
496+ 'no-exist': 'hah invalid',
497+ }
498+ },
499+ 'fail': {
500+ 'name': 'will-fail'
501+ }
502+ }
503+ }
504+ }
505+
506+ request = self.make_request('bundle', remainder='/proof')
507+ request.params = {
508+ 'deployer_file': yaml.dump(bundle_config)
509+ }
510+ response = self.api_class(request)()
511+ self.assertEqual(200, response.status_code)
512+ results = response.json_body
513+ self.assertEqual(1, len(results['errors']))
514+ self.assertEqual(3, len(results['error_messages']))
515+
516+ # One message from not finding a charm.
517+ self.assertEqual(
518+ 'wiki: Could not find charm: fail',
519+ results['error_messages'][0]
520+ )
521+ # The other two from checking the config of the other charm.
522+ self.assertEqual(
523+ 'wiki: script-interval is not of type int.',
524+ results['error_messages'][1]
525+ )
526+ self.assertEqual(
527+ 'wiki: The charm has no option for: no-exist',
528+ results['error_messages'][2]
529+ )
530+
531
532 class TestAPI3Bundles(TestAPIBundles, API3Mixin):
533 """Test API 3 bundle endpoint."""

Subscribers

People subscribed via source and target branches