Merge lp:~tvansteenburgh/charmworldlib/bundle-support into lp:charmworldlib/0.3

Proposed by Tim Van Steenburgh
Status: Merged
Merged at revision: 31
Proposed branch: lp:~tvansteenburgh/charmworldlib/bundle-support
Merge into: lp:charmworldlib/0.3
Diff against target: 953 lines (+761/-63)
7 files modified
charmworldlib/api.py (+21/-0)
charmworldlib/bundle.py (+112/-4)
charmworldlib/charm.py (+12/-21)
tests/test_api.py (+7/-0)
tests/test_bundle.py (+493/-32)
tests/test_bundles.py (+108/-0)
tests/test_charms.py (+8/-6)
To merge this branch: bzr merge lp:~tvansteenburgh/charmworldlib/bundle-support
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
charmers Pending
Review via email: mp+219745@code.launchpad.net

Description of the change

Added Bundle support

To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Yes. LGTM!

review: Approve
31. By Marco Ceppi

[tvansteenburgh] Add Bundle support

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworldlib/api.py'
2--- charmworldlib/api.py 2014-02-04 12:44:47 +0000
3+++ charmworldlib/api.py 2014-05-15 18:32:26 +0000
4@@ -38,6 +38,19 @@
5
6 return r
7
8+ def search(self, endpoint, doctype, criteria=None, limit=None):
9+ if type(criteria) is str:
10+ criteria = {'text': criteria}
11+ else:
12+ criteria = criteria or {}
13+
14+ criteria['text'] = self._doctype_filter(criteria.get('text'), doctype)
15+
16+ if limit and type(limit) is int:
17+ criteria['limit'] = limit
18+
19+ return self.get(endpoint, criteria)['result'] or []
20+
21 def _earl(self):
22 if self.port:
23 return '%s://%s:%s' % (self.protocol, self.server, self.port)
24@@ -49,3 +62,11 @@
25 endpoint = '/%s' % endpoint
26
27 return '%s/api/%s%s' % (self._earl(), self.version, endpoint)
28+
29+ def _doctype_filter(self, text, doctype):
30+ text = (text or '').strip()
31+ if not text:
32+ return doctype
33+ if not text.startswith(doctype + ':'):
34+ return '{}:{}'.format(doctype, text)
35+ return text
36
37=== modified file 'charmworldlib/bundle.py'
38--- charmworldlib/bundle.py 2014-02-28 13:22:33 +0000
39+++ charmworldlib/bundle.py 2014-05-15 18:32:26 +0000
40@@ -1,10 +1,18 @@
41+import json
42 import yaml
43
44 from . import api
45+from .charm import Charm
46+
47+
48+class BundleNotFound(Exception):
49+ pass
50
51
52 class Bundles(api.API):
53- _base_endpoint = 'bundle'
54+ _base_endpoint = {1: 'bundle', 2: 'bundle', 3: 'bundle'}
55+ _base_search_endpoint = {1: 'bundles', 2: 'search', 3: 'search'}
56+ _doctype = 'bundle'
57
58 def proof(self, deployer_contents):
59 if not self.version >= 3:
60@@ -13,10 +21,110 @@
61 if type(deployer_contents) is not dict:
62 raise Exception('Invalid deployer_contents')
63
64- return self.post('%s/proof' % self._base_endpoint,
65+ return self.post('%s/proof' % self._base_endpoint[self.version],
66 {'deployer_file': yaml.dump(deployer_contents)})
67
68+ def bundle(self, basket_name, name, revision=None, owner=None):
69+ data = self.get(self.bundle_endpoint(
70+ basket_name, name, revision, owner))
71+
72+ if 'result' not in data:
73+ return None
74+
75+ bundle_raw = data['result'][0]
76+ return Bundle.from_bundledata(bundle_raw)
77+
78+ def bundle_endpoint(self, basket_name, name, revision=None, owner=None):
79+ if owner and not owner.startswith('~'):
80+ owner = "~%s" % owner
81+
82+ endpoint = self._base_endpoint[self.version]
83+
84+ if owner:
85+ endpoint = "%s/%s" % (endpoint, owner)
86+
87+ endpoint = "%s/%s" % (endpoint, basket_name)
88+
89+ if isinstance(revision, int) and revision >= 0:
90+ endpoint = "%s/%s" % (endpoint, revision)
91+
92+ endpoint = "%s/%s" % (endpoint, name)
93+
94+ return endpoint
95+
96+ def search(self, criteria=None, limit=None):
97+ result = super(Bundles, self).search(
98+ self._base_search_endpoint[self.version],
99+ self._doctype,
100+ criteria=criteria,
101+ limit=limit)
102+ return [Bundle.from_bundledata(bundle) for bundle in result]
103+
104+ def approved(self):
105+ return self.search(criteria={'type': 'approved'})
106+
107
108 class Bundle(object):
109- def __init__(self):
110- pass
111+ @classmethod
112+ def from_bundledata(cls, bundle_data):
113+ bundle = cls()
114+ bundle._parse(bundle_data)
115+
116+ return bundle
117+
118+ def __init__(self, bundle_id=None):
119+ self.charms = {}
120+ self._raw = {}
121+ self._api = api.API()
122+
123+ if bundle_id:
124+ self._fetch(bundle_id)
125+
126+ def __getattr__(self, key):
127+ return self._raw[key]
128+
129+ def _fetch(self, bundle_id):
130+ b = Bundles()
131+ owner, basket_name, revision, name = self._parse_id(bundle_id)
132+ try:
133+ data = self._api.get(b.bundle_endpoint(basket_name, name,
134+ revision=revision,
135+ owner=owner))
136+ self._parse(data)
137+ except Exception as e:
138+ raise BundleNotFound('API request failed: %s' % str(e))
139+
140+ def _parse(self, bundle_data):
141+ self._raw = bundle_data
142+
143+ for charm_name, charm_data in bundle_data['charm_metadata'].items():
144+ self.charms[charm_name] = Charm.from_charmdata({
145+ 'charm': charm_data})
146+
147+ def _parse_id(self, bundle_id):
148+ owner, revision = None, None
149+
150+ parts = bundle_id.split('/')
151+
152+ if len(parts) == 4:
153+ owner, basket_name, revision, name = parts
154+ elif len(parts) == 3:
155+ if bundle_id.startswith('~'):
156+ owner, basket_name, name = parts
157+ else:
158+ basket_name, revision, name = parts
159+ elif len(parts) == 2:
160+ basket_name, name = parts
161+ else:
162+ raise ValueError('Invalid bundle id: {}'.format(bundle_id))
163+
164+ if revision:
165+ revision = int(revision)
166+
167+ return owner, basket_name, revision, name
168+
169+ def __str__(self):
170+ return json.dumps(self._raw, indent=2)
171+
172+ def __repr__(self):
173+ return '<Bundle %s>' % self.id
174
175=== modified file 'charmworldlib/charm.py'
176--- charmworldlib/charm.py 2014-02-04 16:24:43 +0000
177+++ charmworldlib/charm.py 2014-05-15 18:32:26 +0000
178@@ -1,4 +1,3 @@
179-
180 import re
181 import json
182 from . import api
183@@ -40,6 +39,7 @@
184 class Charms(api.API):
185 _base_endpoint = {1: 'charm', 2: 'charm', 3: 'charm'}
186 _base_search_endpoint = {1: 'charms', 2: 'charms', 3: 'search'}
187+ _doctype = 'charm'
188
189 def requires(self, interfaces=[], limit=None):
190 return self.interfaces(requires=interfaces)
191@@ -73,26 +73,6 @@
192 charm_raw = data['result'][0]
193 return Charm.from_charmdata(charm_raw)
194
195- def approved(self):
196- return self.search({'type': 'approved'})
197-
198- def search(self, criteria={}, limit=None):
199- results = []
200- if type(criteria) is str:
201- criteria = {'text': criteria}
202- if limit and type(limit) is int:
203- criteria['limit'] = limit
204-
205- data = self.get(self._base_search_endpoint[self.version], criteria)
206-
207- if not data['result']:
208- return []
209-
210- for charm in data['result']:
211- results.append(Charm.from_charmdata(charm))
212-
213- return results
214-
215 def charm_endpoint(self, name, series='precise', revision=None,
216 owner=None):
217 if owner and not owner.startswith('~'):
218@@ -110,6 +90,17 @@
219
220 return endpoint
221
222+ def search(self, criteria=None, limit=None):
223+ result = super(Charms, self).search(
224+ self._base_search_endpoint[self.version],
225+ self._doctype,
226+ criteria=criteria,
227+ limit=limit)
228+ return [Charm.from_charmdata(charm) for charm in result]
229+
230+ def approved(self):
231+ return self.search(criteria={'type': 'approved'})
232+
233
234 class Charm(object):
235 @classmethod
236
237=== modified file 'tests/test_api.py'
238--- tests/test_api.py 2014-01-20 17:43:03 +0000
239+++ tests/test_api.py 2014-05-15 18:32:26 +0000
240@@ -78,3 +78,10 @@
241 def test_post_params(self, mfetch):
242 self.a.post('end/of/the/world', {'isit'})
243 mfetch.assert_called_with('end/of/the/world', {'isit'}, 'post')
244+
245+ def test_doctype_filter(self):
246+ f = self.a._doctype_filter
247+ self.assertEqual(f(None, 'charm'), 'charm')
248+ self.assertEqual(f(' ', 'charm'), 'charm')
249+ self.assertEqual(f('searchterm', 'charm'), 'charm:searchterm')
250+ self.assertEqual(f(' charm:searchterm', 'charm'), 'charm:searchterm')
251
252=== modified file 'tests/test_bundle.py'
253--- tests/test_bundle.py 2014-02-28 13:22:33 +0000
254+++ tests/test_bundle.py 2014-05-15 18:32:26 +0000
255@@ -1,38 +1,499 @@
256-"""Unit test for API"""
257+"""Unit test for Bundle"""
258
259+import json
260+from mock import patch
261 import unittest
262
263-from mock import patch
264-from charmworldlib.bundle import (
265- Bundle,
266- Bundles,
267-)
268-
269-
270-class BundlesTest(unittest.TestCase):
271- @classmethod
272- def setUpClass(cls):
273- cls.b = Bundles()
274-
275- @patch('charmworldlib.bundle.api.API.post')
276- def test_bundles_proof(self, mpost):
277- mpost.return_value = {'deploy'}
278- self.b.proof({'deployment': {}})
279- mpost.assert_called_with('bundle/proof', {'deployer_file':
280- 'deployment: {}\n'})
281-
282- def test_bundles_proof_ver(self):
283- b = Bundles(version=2)
284- self.assertRaises(ValueError, b.proof, 'garbage')
285-
286- def test_bundles_proof_invalid(self):
287- self.assertRaises(Exception, self.b.proof, 'garbage')
288+from charmworldlib.bundle import Bundle, BundleNotFound
289+
290+BUNDLE_DATA = json.loads(r'''
291+{
292+ "basket_name": "mediawiki",
293+ "basket_revision": 6,
294+ "branch_deleted": false,
295+ "branch_spec": "~charmers/charms/bundles/mediawiki/bundle",
296+ "changes": [
297+ {
298+ "authors": [
299+ "Jorge O. Castro <jorge@ubuntu.com>"
300+ ],
301+ "committer": "Jorge O. Castro <jorge@ubuntu.com>",
302+ "created": 1383229159.809,
303+ "message": "Initial commit\n",
304+ "revno": 1
305+ }
306+ ],
307+ "charm_metadata": {
308+ "mediawiki": {
309+ "annotations": {
310+ "gui-x": 609,
311+ "gui-y": -15
312+ },
313+ "categories": [
314+ "applications"
315+ ],
316+ "code_source": {
317+ "bugs_link": "https://bugs.launchpad.net/charms/+source/mediawiki",
318+ "last_log": "merging lp:~dave-cheney/charms/precise/mediawiki/trunk as per https://code.launchpad.net/~dave-cheney/charms/precise/mediawiki/trunk/+merge/182803",
319+ "location": "lp:~charmers/charms/precise/mediawiki/trunk",
320+ "revision": "72",
321+ "revisions": [
322+ {
323+ "authors": [
324+ {
325+ "email": "clint@ubuntu.com",
326+ "name": "Clint Byrum"
327+ }
328+ ],
329+ "date": "2012-06-28T00:02:47Z",
330+ "message": "removing old broken munin bits",
331+ "revno": 63
332+ }
333+ ],
334+ "type": "bzr"
335+ },
336+ "date_created": "2012-04-16T18:29:51Z",
337+ "description": "MediaWiki is a wiki engine (a program for creating a collaboratively\nedited website). It is designed to handle heavy websites containing\nlibrary-like document collections, and supports user uploads of\nimages/sounds, multilingual content, TOC autogeneration, ISBN links,\netc.\n",
338+ "distro_series": "precise",
339+ "downloads": 3928,
340+ "downloads_in_past_30_days": 484,
341+ "files": [
342+ "hooks/slave-relation-departed",
343+ "hooks/combine-dbservers",
344+ "hooks/cache-relation-changed",
345+ "hooks/website-relation-joined",
346+ "revision",
347+ "icon.svg",
348+ "hooks/upgrade-charm",
349+ "hooks/stop",
350+ "README.md",
351+ "hooks/db-relation-changed",
352+ "hooks/db-relation-departed",
353+ "hooks/install",
354+ "metadata.yaml",
355+ "hooks/config-changed",
356+ "hooks/slave-relation-changed",
357+ "config.yaml",
358+ "hooks/slave-relation-broken"
359+ ],
360+ "id": "precise/mediawiki-10",
361+ "is_approved": true,
362+ "is_subordinate": false,
363+ "maintainer": {
364+ "email": "clint@ubuntu.com",
365+ "name": "Clint Byrum"
366+ },
367+ "maintainers": [
368+ {
369+ "email": "clint@ubuntu.com",
370+ "name": "Clint Byrum"
371+ }
372+ ],
373+ "name": "mediawiki",
374+ "options": {
375+ "admins": {
376+ "description": "Admin users to create, user:pass",
377+ "type": "string"
378+ },
379+ "debug": {
380+ "default": false,
381+ "description": "turn on debugging features of mediawiki",
382+ "type": "boolean"
383+ },
384+ "logo": {
385+ "description": "URL to fetch logo from",
386+ "type": "string"
387+ },
388+ "name": {
389+ "default": "Please set name of wiki",
390+ "description": "The name, or Title of the Wiki",
391+ "type": "string"
392+ },
393+ "skin": {
394+ "default": "vector",
395+ "description": "skin for the Wiki",
396+ "type": "string"
397+ }
398+ },
399+ "owner": "charmers",
400+ "rating_denominator": 0,
401+ "rating_numerator": 0,
402+ "relations": {
403+ "provides": {
404+ "website": {
405+ "interface": "http"
406+ }
407+ },
408+ "requires": {
409+ "cache": {
410+ "interface": "memcache"
411+ },
412+ "db": {
413+ "interface": "mysql"
414+ },
415+ "slave": {
416+ "interface": "mysql"
417+ }
418+ }
419+ },
420+ "revision": 90,
421+ "summary": "Website engine for collaborative work",
422+ "tested_providers": {
423+ "ec2": "SUCCESS",
424+ "openstack": "SUCCESS"
425+ },
426+ "url": "cs:precise/mediawiki-10"
427+ },
428+ "mysql": {
429+ "annotations": {
430+ "gui-x": 610,
431+ "gui-y": 255
432+ },
433+ "categories": [
434+ "databases"
435+ ],
436+ "code_source": {
437+ "bugs_link": "https://bugs.launchpad.net/charms/+source/mysql",
438+ "last_log": "Updated README",
439+ "location": "lp:~charmers/charms/precise/mysql/trunk",
440+ "revision": "105",
441+ "revisions": [
442+ {
443+ "authors": [
444+ {
445+ "email": "marco@ceppi.net",
446+ "name": "Marco Ceppi"
447+ }
448+ ],
449+ "date": "2013-04-25T18:19:45Z",
450+ "message": "Added icon.svg",
451+ "revno": 96
452+ }
453+ ],
454+ "type": "bzr"
455+ },
456+ "date_created": "2012-04-16T18:30:00Z",
457+ "description": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database\nserver. SQL (Structured Query Language) is the most popular database query\nlanguage in the world. The main goals of MySQL are speed, robustness and\nease of use.\n",
458+ "distro_series": "precise",
459+ "downloads": 21895,
460+ "downloads_in_past_30_days": 2006,
461+ "files": [
462+ "hooks/munin-relation-joined",
463+ "hooks/monitors.common.bash",
464+ "hooks/db-relation-joined",
465+ "hooks/shared-db-relation-changed",
466+ "hooks/master-relation-departed",
467+ "hooks/monitors-relation-departed",
468+ "hooks/master-relation-broken",
469+ "hooks/lib/cluster_utils.py",
470+ "hooks/shared_db_relations.py",
471+ "hooks/slave-relation-broken",
472+ "hooks/lib/utils.py",
473+ "hooks/ha-relation-changed",
474+ "hooks/munin-relation-changed",
475+ "hooks/common.py",
476+ "hooks/start",
477+ "hooks/config-changed",
478+ "hooks/db-relation-broken",
479+ "hooks/slave-relation-changed",
480+ "hooks/shared-db-relation-joined",
481+ "hooks/ha_relations.py",
482+ "hooks/cluster-relation-changed",
483+ "hooks/slave-relation-departed",
484+ "hooks/lib/ceph_utils.py",
485+ "hooks/ceph-relation-changed",
486+ "metadata.yaml",
487+ "hooks/ha-relation-joined",
488+ "hooks/stop",
489+ "hooks/db-admin-relation-joined",
490+ "config.yaml",
491+ "hooks/monitors-relation-joined",
492+ "icon.svg",
493+ "hooks/upgrade-charm",
494+ "README.md",
495+ "hooks/ceph-relation-joined",
496+ "hooks/master-relation-changed",
497+ "hooks/lib/__init__.py",
498+ "hooks/slave-relation-joined",
499+ "hooks/install",
500+ "hooks/local-monitors-relation-joined",
501+ "revision",
502+ "hooks/monitors-relation-broken"
503+ ],
504+ "id": "precise/mysql-28",
505+ "is_approved": true,
506+ "is_subordinate": false,
507+ "maintainer": {
508+ "email": "marco@ceppi.net",
509+ "name": "Marco Ceppi"
510+ },
511+ "maintainers": [
512+ {
513+ "email": "marco@ceppi.net",
514+ "name": "Marco Ceppi"
515+ }
516+ ],
517+ "name": "mysql",
518+ "options": {
519+ "binlog-format": {
520+ "default": "MIXED",
521+ "description": "If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast.",
522+ "type": "string"
523+ },
524+ "block-size": {
525+ "default": 5,
526+ "description": "Default block storage size to create when setting up MySQL block storage.\nThis value should be specified in GB (e.g. 100 not 100GB).\n",
527+ "type": "int"
528+ },
529+ "dataset-size": {
530+ "default": "80%",
531+ "description": "How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Any more specific settings will override these defaults though. This currently sets innodb_buffer_pool_size or key_cache_size depending on the setting in preferred-storage-engine. If query-cache-type is set to 'ON' or 'DEMAND' 20% of this is given to query-cache-size. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset and (if enabled) query cache.",
532+ "type": "string"
533+ },
534+ "flavor": {
535+ "default": "distro",
536+ "description": "Possible values are 'distro' or 'percona'",
537+ "type": "string"
538+ },
539+ "ha-bindiface": {
540+ "default": "eth0",
541+ "description": "Default network interface on which HA cluster will bind to communication\nwith the other members of the HA Cluster.\n",
542+ "type": "string"
543+ },
544+ "ha-mcastport": {
545+ "default": 5411,
546+ "description": "Default multicast port number that will be used to communicate between\nHA Cluster nodes.\n",
547+ "type": "int"
548+ },
549+ "max-connections": {
550+ "default": -1,
551+ "description": "Maximum connections to allow. -1 means use the server's compiled in default.",
552+ "type": "int"
553+ },
554+ "preferred-storage-engine": {
555+ "default": "InnoDB",
556+ "description": "Tune the server for usage of this storage engine. Other possible value is MyISAM. Comma separated will cause settings to split resources evenly among given engines.",
557+ "type": "string"
558+ },
559+ "query-cache-size": {
560+ "default": -1,
561+ "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.",
562+ "type": "int"
563+ },
564+ "query-cache-type": {
565+ "default": "OFF",
566+ "description": "Query cache is usually a good idea, but can hurt concurrency. Valid values are \"OFF\", \"ON\", or \"DEMAND\". http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_query_cache_type",
567+ "type": "string"
568+ },
569+ "rbd-name": {
570+ "default": "mysql1",
571+ "description": "The name that will be used to create the Ceph's RBD image with. If the\nimage name exists in Ceph, it will be re-used and the data will be\noverwritten.\n",
572+ "type": "string"
573+ },
574+ "tuning-level": {
575+ "default": "safest",
576+ "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.",
577+ "type": "string"
578+ },
579+ "vip": {
580+ "description": "Virtual IP to use to front mysql in ha configuration",
581+ "type": "string"
582+ },
583+ "vip_cidr": {
584+ "default": 24,
585+ "description": "Netmask that will be used for the Virtual IP",
586+ "type": "int"
587+ },
588+ "vip_iface": {
589+ "default": "eth0",
590+ "description": "Network Interface where to place the Virtual IP",
591+ "type": "string"
592+ }
593+ },
594+ "owner": "charmers",
595+ "rating_denominator": 0,
596+ "rating_numerator": 0,
597+ "relations": {
598+ "provides": {
599+ "db": {
600+ "interface": "mysql"
601+ },
602+ "db-admin": {
603+ "interface": "mysql-root"
604+ },
605+ "local-monitors": {
606+ "interface": "local-monitors",
607+ "scope": "container"
608+ },
609+ "master": {
610+ "interface": "mysql-oneway-replication"
611+ },
612+ "monitors": {
613+ "interface": "monitors"
614+ },
615+ "munin": {
616+ "interface": "munin-node"
617+ },
618+ "shared-db": {
619+ "interface": "mysql-shared"
620+ }
621+ },
622+ "requires": {
623+ "ceph": {
624+ "interface": "ceph-client"
625+ },
626+ "ha": {
627+ "interface": "hacluster",
628+ "scope": "container"
629+ },
630+ "slave": {
631+ "interface": "mysql-oneway-replication"
632+ }
633+ }
634+ },
635+ "revision": 309,
636+ "summary": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database",
637+ "tested_providers": {},
638+ "url": "cs:precise/mysql-28"
639+ }
640+ },
641+ "data": {
642+ "relations": [
643+ [
644+ "mediawiki:db",
645+ "mysql:db"
646+ ]
647+ ],
648+ "series": "precise",
649+ "services": {
650+ "mediawiki": {
651+ "annotations": {
652+ "gui-x": 609,
653+ "gui-y": -15
654+ },
655+ "charm": "cs:precise/mediawiki-10",
656+ "num_units": 1,
657+ "options": {
658+ "debug": false,
659+ "name": "Please set name of wiki",
660+ "skin": "vector"
661+ }
662+ },
663+ "mysql": {
664+ "annotations": {
665+ "gui-x": 610,
666+ "gui-y": 255
667+ },
668+ "charm": "cs:precise/mysql-28",
669+ "num_units": 1,
670+ "options": {
671+ "binlog-format": "MIXED",
672+ "block-size": 5,
673+ "dataset-size": "80%",
674+ "flavor": "distro",
675+ "ha-bindiface": "eth0",
676+ "ha-mcastport": 5411,
677+ "max-connections": -1,
678+ "preferred-storage-engine": "InnoDB",
679+ "query-cache-size": -1,
680+ "query-cache-type": "OFF",
681+ "rbd-name": "mysql1",
682+ "tuning-level": "safest",
683+ "vip_cidr": 24,
684+ "vip_iface": "eth0"
685+ }
686+ }
687+ }
688+ },
689+ "deployer_file_url": "http://manage.jujucharms.com/bundle/%7Echarmers/mediawiki/6/single/json",
690+ "description": "",
691+ "downloads": 58,
692+ "downloads_in_past_30_days": 4,
693+ "files": [
694+ "README.md",
695+ "bundles.yaml"
696+ ],
697+ "first_change": {
698+ "authors": [
699+ "Jorge O. Castro <jorge@ubuntu.com>"
700+ ],
701+ "committer": "Jorge O. Castro <jorge@ubuntu.com>",
702+ "created": 1383229159.809,
703+ "message": "Initial commit\n",
704+ "revno": 1
705+ },
706+ "id": "~charmers/mediawiki/6/single",
707+ "last_change": {
708+ "authors": [
709+ "Jorge O. Castro <jorge@ubuntu.com>"
710+ ],
711+ "committer": "Jorge O. Castro <jorge@ubuntu.com>",
712+ "created": 1394724884.008,
713+ "message": "Combine single and scalable bundles into one.\n",
714+ "revno": 6
715+ },
716+ "name": "single",
717+ "owner": "charmers",
718+ "permanent_url": "bundle:~charmers/mediawiki/6/single",
719+ "promulgated": true,
720+ "title": ""
721+}''')
722
723
724 class BundleTest(unittest.TestCase):
725- @classmethod
726- def setUpClass(cls):
727- cls.b = Bundle()
728-
729- def test_bundle(self):
730- pass
731+ def test_from_bundledata(self):
732+ b = Bundle.from_bundledata(BUNDLE_DATA)
733+ self.assertIsInstance(b, Bundle)
734+ self.assertEqual(b._raw, BUNDLE_DATA)
735+
736+ @patch('charmworldlib.bundle.api.API.get')
737+ def test_init(self, get):
738+ get.return_value = BUNDLE_DATA
739+ self.assertEqual(
740+ Bundle('~me/mediawiki/0/single')._raw,
741+ Bundle.from_bundledata(BUNDLE_DATA)._raw)
742+
743+ def test_getattr(self):
744+ b = Bundle.from_bundledata(BUNDLE_DATA)
745+ for k, v in b._raw.items():
746+ self.assertEqual(v, getattr(b, k))
747+
748+ @patch('charmworldlib.bundle.api.API.get')
749+ def test_bundle_fetch_fail(self, mget):
750+ msg = 'Request failed with: 500'
751+ mget.side_effect = Exception(msg)
752+ self.assertRaises(BundleNotFound, Bundle, bundle_id='mediawiki/single')
753+
754+ def test_parse(self):
755+ from charmworldlib.charm import Charm
756+ b = Bundle.from_bundledata(BUNDLE_DATA)
757+ self.assertEqual(len(b.charms), 2)
758+ self.assertTrue('mediawiki' in b.charms)
759+ self.assertTrue('mysql' in b.charms)
760+ for charm in b.charms.values():
761+ self.assertIsInstance(charm, Charm)
762+
763+ def test_parse_id(self):
764+ b = Bundle()
765+
766+ self.assertEqual(
767+ b._parse_id('~me/mediawiki/0/single'),
768+ ('~me', 'mediawiki', 0, 'single'))
769+ self.assertEqual(
770+ b._parse_id('~me/mediawiki/single'),
771+ ('~me', 'mediawiki', None, 'single'))
772+ self.assertEqual(
773+ b._parse_id('mediawiki/0/single'),
774+ (None, 'mediawiki', 0, 'single'))
775+ self.assertEqual(
776+ b._parse_id('mediawiki/single'),
777+ (None, 'mediawiki', None, 'single'))
778+ self.assertRaises(ValueError, b._parse_id, 'mediawiki')
779+
780+ def test_str(self):
781+ b = Bundle.from_bundledata(BUNDLE_DATA)
782+ self.assertEqual(str(b), json.dumps(BUNDLE_DATA, indent=2))
783+
784+ def test_repr(self):
785+ b = Bundle.from_bundledata(BUNDLE_DATA)
786+ self.assertEqual(repr(b), '<Bundle ~charmers/mediawiki/6/single>')
787
788=== added file 'tests/test_bundles.py'
789--- tests/test_bundles.py 1970-01-01 00:00:00 +0000
790+++ tests/test_bundles.py 2014-05-15 18:32:26 +0000
791@@ -0,0 +1,108 @@
792+"""Unit test for Bundles"""
793+
794+import unittest
795+
796+from mock import patch
797+from charmworldlib.bundle import Bundles
798+
799+
800+class BundlesTest(unittest.TestCase):
801+ @classmethod
802+ def setUpClass(cls):
803+ cls.b = Bundles()
804+
805+ @patch('charmworldlib.bundle.api.API.post')
806+ def test_bundles_proof(self, mpost):
807+ mpost.return_value = {'deploy'}
808+ self.b.proof({'deployment': {}})
809+ mpost.assert_called_with('bundle/proof', {'deployer_file':
810+ 'deployment: {}\n'})
811+
812+ def test_bundles_proof_ver(self):
813+ b = Bundles(version=2)
814+ self.assertRaises(ValueError, b.proof, 'garbage')
815+
816+ def test_bundles_proof_invalid(self):
817+ self.assertRaises(Exception, self.b.proof, 'garbage')
818+
819+ @patch('charmworldlib.bundle.Bundle')
820+ @patch('charmworldlib.bundle.Bundles.get')
821+ def test_bundles_search(self, mget, mBundle):
822+ cdata = {'bundle': {'id': 'oneiric/bundle-0'}}
823+ mget.return_value = {'result': [cdata]}
824+ self.assertEqual([mBundle.from_bundledata()], self.b.search())
825+ mget.assert_called_with('search', {'text': 'bundle'})
826+ mBundle.from_bundledata.assert_called_with(cdata)
827+
828+ @patch('charmworldlib.bundle.Bundles.get')
829+ def test_bundles_search_string(self, mget):
830+ mget.return_value = {'result': None}
831+ self.assertEqual([], self.b.search('blurb', 2))
832+ mget.assert_called_with('search', {'text': 'bundle:blurb', 'limit': 2})
833+
834+ @patch('charmworldlib.bundle.Bundles.get')
835+ def test_bundles_search_params(self, mget):
836+ mget.return_value = {'result': None}
837+ self.assertEqual([], self.b.search({'approved': True}, 1))
838+ mget.assert_called_with(
839+ 'search', {'approved': True, 'limit': 1, 'text': 'bundle'})
840+
841+ @patch('charmworldlib.bundle.Bundles.get')
842+ def test_bundles_search_no_results(self, mget):
843+ mget.return_value = {'result': None}
844+ self.assertEqual([], self.b.search('no-match'))
845+ mget.assert_called_with('search', {'text': 'bundle:no-match'})
846+
847+ @patch('charmworldlib.bundle.Bundle')
848+ @patch('charmworldlib.bundle.Bundles.get')
849+ def test_bundles_search_versions(self, mget, mBundle):
850+ self.b.version = 2
851+ cdata = {'bundle': {'id': 'mediawiki/0/single'}}
852+ mget.return_value = {'result': [cdata]}
853+ self.assertEqual([mBundle.from_bundledata()], self.b.search())
854+ mget.assert_called_with('search', {'text': 'bundle'})
855+ mBundle.from_bundledata.assert_called_with(cdata)
856+
857+ @patch('charmworldlib.bundle.Bundles.get')
858+ def test_bundles_approved(self, mget):
859+ mget.return_value = {'result': None}
860+ self.assertEqual([], self.b.approved())
861+ mget.assert_called_with(
862+ 'search', {'type': 'approved', 'text': 'bundle'})
863+
864+ @patch('charmworldlib.bundle.Bundle.from_bundledata')
865+ @patch('charmworldlib.bundle.Bundles.get')
866+ def test_bundles_bundle(self, mget, mBundle):
867+ cdata = {'bundle': {'id': 'mediawiki/0/single'}}
868+ mget.return_value = {'result': [cdata]}
869+ self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
870+ revision=0))
871+ mget.assert_called_with('bundle/mediawiki/0/single')
872+ mBundle.assert_called_with(cdata)
873+
874+ @patch('charmworldlib.bundle.Bundle.from_bundledata')
875+ @patch('charmworldlib.bundle.Bundles.get')
876+ def test_bundles_bundle_full(self, mget, mBundle):
877+ cdata = {'bundle': {'id': '~me/mediawiki/0/single'}}
878+ mget.return_value = {'result': [cdata]}
879+ self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
880+ revision=0, owner='~me'))
881+ mget.assert_called_with('bundle/~me/mediawiki/0/single')
882+ mBundle.assert_called_with(cdata)
883+
884+ @patch('charmworldlib.bundle.Bundles.get')
885+ def test_bundles_bundle_404(self, mget):
886+ mget.return_value = {'none': None}
887+ self.assertEqual(None, self.b.bundle('mediawiki', 'single',
888+ revision=0, owner='~me'))
889+ mget.assert_called_with('bundle/~me/mediawiki/0/single')
890+
891+ @patch('charmworldlib.bundle.Bundle.from_bundledata')
892+ @patch('charmworldlib.bundle.Bundles.get')
893+ def test_bundles_bundle_full_owner(self, mget, mBundle):
894+ cdata = {'bundle': {'id': '~me/mediawiki/0/single'}}
895+ mget.return_value = {'result': [cdata]}
896+ self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single',
897+ revision=0, owner='me'))
898+ mget.assert_called_with('bundle/~me/mediawiki/0/single')
899+ mBundle.assert_called_with(cdata)
900
901=== modified file 'tests/test_charms.py'
902--- tests/test_charms.py 2014-01-24 17:15:21 +0000
903+++ tests/test_charms.py 2014-05-15 18:32:26 +0000
904@@ -17,26 +17,27 @@
905 cdata = {'charm': {'id': 'oneiric/charm-0'}}
906 mget.return_value = {'result': [cdata]}
907 self.assertEqual([mCharm.from_charmdata()], self.c.search())
908- mget.assert_called_with('search', {})
909+ mget.assert_called_with('search', {'text': 'charm'})
910 mCharm.from_charmdata.assert_called_with(cdata)
911
912 @patch('charmworldlib.charm.Charms.get')
913 def test_charms_search_string(self, mget):
914 mget.return_value = {'result': None}
915 self.assertEqual([], self.c.search('blurb', 2))
916- mget.assert_called_with('search', {'text': 'blurb', 'limit': 2})
917+ mget.assert_called_with('search', {'text': 'charm:blurb', 'limit': 2})
918
919 @patch('charmworldlib.charm.Charms.get')
920 def test_charms_search_params(self, mget):
921 mget.return_value = {'result': None}
922 self.assertEqual([], self.c.search({'approved': True}, 1))
923- mget.assert_called_with('search', {'approved': True, 'limit': 1})
924+ mget.assert_called_with(
925+ 'search', {'approved': True, 'limit': 1, 'text': 'charm'})
926
927 @patch('charmworldlib.charm.Charms.get')
928 def test_charms_search_no_results(self, mget):
929 mget.return_value = {'result': None}
930 self.assertEqual([], self.c.search('no-match'))
931- mget.assert_called_with('search', {'text': 'no-match'})
932+ mget.assert_called_with('search', {'text': 'charm:no-match'})
933
934 @patch('charmworldlib.charm.Charm')
935 @patch('charmworldlib.charm.Charms.get')
936@@ -45,14 +46,15 @@
937 cdata = {'charm': {'id': 'oneiric/charm-0'}}
938 mget.return_value = {'result': [cdata]}
939 self.assertEqual([mCharm.from_charmdata()], self.c.search())
940- mget.assert_called_with('charms', {})
941+ mget.assert_called_with('charms', {'text': 'charm'})
942 mCharm.from_charmdata.assert_called_with(cdata)
943
944 @patch('charmworldlib.charm.Charms.get')
945 def test_charms_approved(self, mget):
946 mget.return_value = {'result': None}
947 self.assertEqual([], self.c.approved())
948- mget.assert_called_with('search', {'type': 'approved'})
949+ mget.assert_called_with(
950+ 'search', {'type': 'approved', 'text': 'charm'})
951
952 @patch('charmworldlib.charm.Charm.from_charmdata')
953 @patch('charmworldlib.charm.Charms.get')

Subscribers

People subscribed via source and target branches

to all changes: