diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-07-27 21:38:40 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-07-27 21:38:40 (GMT) |
commit | f68b009e2260ddf6d97eb0fa2ee7d15cf695aeea (patch) | |
tree | 92ba7d93a28b11cfe7c37964dc94fc243123d432 | |
parent | fa35499bcd89260b436c3c9f03c887661983c14d (diff) |
Add release API command to easily upload new implementation
-rwxr-xr-x | misc/aslo-sync | 83 | ||||
-rwxr-xr-x | sugar-network | 149 | ||||
-rw-r--r-- | sugar_network/client/cache.py | 9 | ||||
-rw-r--r-- | sugar_network/model/implementation.py | 5 | ||||
-rw-r--r-- | sugar_network/node/routes.py | 74 | ||||
-rw-r--r-- | sugar_network/toolkit/http.py | 8 | ||||
-rwxr-xr-x | tests/integration/node_client.py | 75 | ||||
-rwxr-xr-x | tests/units/node/node.py | 81 |
8 files changed, 348 insertions, 136 deletions
diff --git a/misc/aslo-sync b/misc/aslo-sync index f76135a..a260632 100755 --- a/misc/aslo-sync +++ b/misc/aslo-sync @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (C) 2012 Aleksey Lim +# Copyright (C) 2012-2013 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -30,7 +30,8 @@ from sugar_network.node import data_root from sugar_network.client.bundle import Bundle from sugar_network.resources.volume import Volume from sugar_network.node.slave import SlaveCommands -from sugar_network.toolkit import util, licenses, application, Option +from sugar_network.node.commands import load_activity_bundle +from sugar_network.toolkit import util, licenses, application, spec, Option DOWNLOAD_URL = 'http://download.sugarlabs.org/activities' @@ -351,7 +352,6 @@ class Application(application.Application): SELECT versions.id, versions.version, - addons.status, licenses.name, (select max(localized_string) from translations where id=licenses.text), @@ -377,7 +377,7 @@ class Application(application.Application): recent_version = None recent_filename = None - for version_id, version, status, license_id, alicense, release_date, \ + for version_id, version, license_id, alicense, release_date, \ releasenotes, filename, sugar_min, sugar_max \ in self.sqlexec(sql): if version_id in IGNORE_VERSIONS: @@ -430,11 +430,9 @@ class Application(application.Application): try: self.sync_implementaiton(bundle_id, addon_id, filename, - sugar_min, sugar_max, - stability='stable' if status == 4 else 'developer', - date=int(time.mktime(release_date.timetuple())), - notes=self.get_i18n_field(releasenotes), - license=alicense if alicense else [], + sugar_min, sugar_max, status, alicense, + self.get_i18n_field(releasenotes), + int(time.mktime(release_date.timetuple())), ) except Exception, error: print '-- Failed to sync %s for %s: %s' % \ @@ -601,52 +599,27 @@ class Application(application.Application): }) def sync_implementaiton(self, context, addon_id, filename, - sugar_min, sugar_max, **impl_props): - bundle = Bundle(join(ACTIVITIES_PATH, str(addon_id), filename)) - spec = bundle.get_spec() - if spec is None: - raise Exception('Bundle does not contain spec file') - - if not impl_props['license']: - impl_props['license'] = self.parse_license(spec['license']) - if not impl_props['license']: - if context in LICENSES_MAP: - impl_props['license'] = LICENSES_MAP[context] - else: - raise Exception('Skip bad license %r' % spec['license']) - - print '-- Add %r version to %r activity' % (spec['version'], context) - - spec.requires[SUGAR_GUID] = { - 'restrictions': [ - (sugar_min, None), - (None, '0.%s' % (int(sugar_max.split('.')[-1]) + 1)), - ], - } - - requires = [] - sugar_min = tuple(util.parse_version(sugar_min)[0]) - sugar_max = tuple(util.parse_version(sugar_max)[0]) - for release, name in SUGAR_RELEASES.items(): - if release >= sugar_min and release <= sugar_max: - requires.append(name) - - impl = {'context': context, - 'version': spec['version'], - 'requires': requires, - 'spec': {'*-*': { - 'commands': spec.commands, - 'requires': spec.requires, - 'extract': bundle.rootdir, - }}, - 'ctime': time.time(), - 'mtime': time.time(), - 'author': self.authors(), - } - impl.update(impl_props) - impl = self.volume['implementation'].create(impl) - self.volume['implementation'].set_blob(impl, 'data', - url='/'.join([DOWNLOAD_URL, str(addon_id), filename])) + sugar_min, sugar_max, status, license, notes, date): + bundle_path = join(ACTIVITIES_PATH, str(addon_id), filename) + with load_activity_bundle(self.volume, bundle_path, + 'sugar>=%s; sugar<=%s' % (sugar_min, sugar_max)) as impl: + if impl['license'] == spec.EMPTY_LICENSE: + if not license and context in LICENSES_MAP: + license = LICENSES_MAP[context] + if not license: + raise Exception('No licenses for %r' % filename) + impl['license'] = license + if 'notes' not in impl: + impl['notes'] = notes + impl['stability'] = 'stable' if status == 4 else 'developer' + impl['ctime'] = date + impl['mtime'] = time.time() + impl['author'] = self.authors() + impl['data']['url'] = \ + '/'.join([DOWNLOAD_URL, str(addon_id), filename]) + impl['data']['blob_size'] = os.stat(bundle_path).st_size + + print '-- Add %r version to %r activity' % (impl['version'], context) def parse_license(self, alicense): for good in licenses.GOOD_LICENSES: diff --git a/sugar-network b/sugar-network index d5adbc3..0cc70e5 100755 --- a/sugar-network +++ b/sugar-network @@ -22,14 +22,17 @@ import shlex import types import locale from json import dumps, loads -from os.path import join, exists +from os.path import join, exists, isfile from gevent import monkey from sugar_network import db, client, toolkit from sugar_network.model import RESOURCES +from sugar_network.client import IPCClient, Client from sugar_network.client.routes import ClientRoutes +from sugar_network.node.routes import load_bundle from sugar_network.toolkit.router import Router, Request, Response +from sugar_network.toolkit.spec import Spec from sugar_network.toolkit import application, coroutine, util from sugar_network.toolkit import Option, BUFFER_SIZE, enforce @@ -57,19 +60,12 @@ offline = Option( default=False, type_cast=Option.bool_cast, action='store_true', name='offline') -object_id = Option( - 'for launch command, identifier of the associated datastore object', - name='object-id', short_option='-o') -uri = Option( - 'for launch command, URI to load', - name='uri', short_option='-u') +_ESCAPE_VALUE_RE = re.compile(r'([^\[\]\{\}0-9][^\]\[\{\}]+)') +_LIST_RE = re.compile(r'\s*[;,:]+\s*') -_ESCAPE_VALUE_RE = re.compile('([^\\[\\]\\{\\}0-9][^\\]\\[\\{\\}]+)') - - -class Client(ClientRoutes, Router): +class ClientRouter(ClientRoutes, Router): def __init__(self): home = db.Volume(client.path('db'), RESOURCES) @@ -100,12 +96,14 @@ class Application(application.Application): os.makedirs(toolkit.cachedir.value) @application.command( - 'launch Sugar activity', - args='BUNDLE_ID [COMMAND-LINE-ARGS]', interspersed_args=False, + 'launch a Sugar activity; the COMMAND-LINE-ARGUMENTS might ' + 'include arguments supported by sugar-activity application', + args='BUNDLE_ID [COMMAND-LINE-ARGUMENTS]', + interspersed_args=False, ) def launch(self): enforce(self.check_for_instance(), 'No sugar-network-client session') - ipc = client.IPCClient() + ipc = IPCClient() enforce(self.args, 'BUNDLE_ID was not specified') bundle_id = self.args.pop(0) @@ -113,20 +111,16 @@ class Application(application.Application): params = {} if self.args: params['args'] = self.args - if object_id.value: - params['object_id'] = object_id.value - if uri.value: - params['uri'] = uri.value ipc.get(['context', bundle_id], cmd='launch', **params) @application.command( - 'clone Sugar activity to ~/Activities directory', + 'clone Sugar activities to ~/Activities directory', args='BUNDLE_ID', ) def clone(self): enforce(self.check_for_instance(), 'No sugar-network-client session') - ipc = client.IPCClient() + ipc = IPCClient() enforce(self.args, 'BUNDLE_ID was not specified') bundle_id = self.args.pop(0) @@ -134,31 +128,84 @@ class Application(application.Application): ipc.put(['context', bundle_id], 1, cmd='clone') @application.command( - 'send POST API request') - def POST(self): + 'upload new implementaion for a context; if BUNDLE_PATH points ' + 'not to a .xo bundle, specify all implementaion PROPERTYs for the ' + 'new release (at least context and version)', + args='BUNDLE_PATH [PROPERTY=VALUE]', + ) + def release(self): + enforce(self.args, 'BUNDLE_PATH was not specified') + path = self.args.pop(0) + enforce(isfile(path), 'Cannot open bundle') + + props = {'tags': []} + self._parse_args(props['tags'], props) + if 'license' in props: + value = [i for i in _LIST_RE.split(props['license'].strip()) if i] + props['license'] = value + + guid = self._connect().upload(['implementation'], path, cmd='release') + + if porcelain.value: + print 'Uploaded new release, %s' % guid + else: + print dumps(guid) + + @application.command( + 'send raw API POST request; ' + 'specifies all ARGUMENTs the particular API call requires', + args='PATH [ARGUMENT=VALUE]') + def post(self): self._request('POST', True) + @application.command(hidden=True) + def POST(self): + self.post() + + @application.command( - 'send PUT API request') - def PUT(self): + 'send raw API PUT request; ' + 'specifies all ARGUMENTs the particular API call requires', + args='PATH [ARGUMENT=VALUE]') + def put(self): self._request('PUT', True) + @application.command(hidden=True) + def PUT(self): + self.put() + @application.command( - 'send DELETE API request') - def DELETE(self): + 'send raw API DELETE request', + args='PATH') + def delete(self): self._request('DELETE', False) + @application.command(hidden=True) + def DELETE(self): + self.delete() + @application.command( - 'send GET API request') - def GET(self): + 'send raw API GET request; ' + 'specifies all ARGUMENTs the particular API call requires', + args='PATH [ARGUMENT=VALUE]') + def get(self): self._request('GET', False) + @application.command(hidden=True) + def GET(self): + self.get() + + def _connect(self): + if self.check_for_instance(): + return IPCClient() + else: + return Client(client.api_url.value) + def _request(self, method, post): request = Request(method=method) request.allow_redirects = True request.accept_encoding = '' response = Response() - reply = [] if post: if post_data.value is None and post_file.value is None: @@ -184,35 +231,20 @@ class Application(application.Application): if self.args and self.args[0].startswith('/'): request.path = self.args.pop(0).strip('/').split('/') - for arg in self.args: - arg = shlex.split(arg) - if not arg: - continue - arg = arg[0] - if '=' not in arg: - reply.append(arg) - continue - arg, value = arg.split('=', 1) - arg = arg.strip() - enforce(arg, 'No argument name in %r expression', arg) - if arg in request: - if isinstance(request[arg], basestring): - request[arg] = [request[arg]] - request[arg].append(value) - else: - request[arg] = value + reply = [] + self._parse_args(reply, request) pid_path = None server = None cp = None try: if self.check_for_instance(): - cp = client.IPCClient() + cp = IPCClient() else: pid_path = self.new_instance() if not client.anonymous.value: util.ensure_key(client.key_path()) - cp = Client() + cp = ClientRouter() result = cp.call(request, response) finally: if server is not None: @@ -258,6 +290,26 @@ class Application(application.Application): else: sys.stdout.write(result) + def _parse_args(self, tags, props): + for arg in self.args: + arg = shlex.split(arg) + if not arg: + continue + arg = arg[0] + if '=' not in arg: + tags.append(arg) + continue + arg, value = arg.split('=', 1) + arg = arg.strip() + enforce(arg, 'No argument name in %r expression', arg) + if arg in props: + if isinstance(props[arg], basestring): + props[arg] = [props[arg]] + props[arg].append(value) + else: + props[arg] = value + + # Let toolkit.http work in concurrence monkey.patch_socket() monkey.patch_select() @@ -272,7 +324,6 @@ toolkit.cachedir.value = client.profile_path('tmp') Option.seek('main', [ application.debug, porcelain, post_data, post_file, json, offline, - object_id, uri, ]) Option.seek('client', [ client.api_url, client.layers, client.ipc_port, client.local_root, diff --git a/sugar_network/client/cache.py b/sugar_network/client/cache.py index 1d8167f..d95b1fc 100644 --- a/sugar_network/client/cache.py +++ b/sugar_network/client/cache.py @@ -56,6 +56,9 @@ def recycle(): def ensure(requested_size=0, temp_size=0): stat = os.statvfs(local_root.value) + if stat.f_blocks == 0: + # TODO Sonds like a tmpfs or so + return total = stat.f_blocks * stat.f_frsize free = stat.f_bfree * stat.f_frsize @@ -112,6 +115,11 @@ def _list(): total = 0 result = [] root = join(local_root.value, 'cache', 'implementation') + + if not exists(root): + os.makedirs(root) + return 0, [] + for filename in os.listdir(root): path = join(root, filename) if not isdir(path): @@ -125,6 +133,7 @@ def _list(): except Exception: toolkit.exception('Cannot list %r cached implementation', path) result.append((0, 0, path)) + return total, sorted(result) diff --git a/sugar_network/model/implementation.py b/sugar_network/model/implementation.py index d0f03f8..55636e3 100644 --- a/sugar_network/model/implementation.py +++ b/sugar_network/model/implementation.py @@ -46,9 +46,8 @@ class Implementation(db.Resource): def version(self, value): return value - @db.indexed_property(prefix='S', - acl=ACL.CREATE | ACL.READ, - typecast=model.STABILITIES) + @db.indexed_property(prefix='S', default='stabile', + acl=ACL.CREATE | ACL.READ, typecast=model.STABILITIES) def stability(self, value): return value diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index 9a416d8..5ef9d7c 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -14,8 +14,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import shutil import logging import hashlib +from contextlib import contextmanager from ConfigParser import ConfigParser from os.path import join, isdir, exists @@ -23,8 +25,10 @@ from sugar_network import db, node, toolkit, model from sugar_network.node import stats_node, stats_user from sugar_network.toolkit.router import route, preroute, postroute from sugar_network.toolkit.router import ACL, fallbackroute +from sugar_network.toolkit.spec import EMPTY_LICENSE from sugar_network.toolkit.spec import parse_requires, ensure_requires -from sugar_network.toolkit import http, coroutine, enforce +from sugar_network.toolkit.bundle import Bundle +from sugar_network.toolkit import http, coroutine, util, enforce _MAX_STATS_LENGTH = 100 @@ -131,6 +135,16 @@ class NodeRoutes(db.Routes, model.Routes): else: return toolkit.iter_file(path) + @route('POST', ['implementation'], cmd='release', + mime_type='application/json') + def release(self, request, document): + with util.NamedTemporaryFile() as blob: + shutil.copyfileobj(request.content_stream, blob) + blob.flush() + with load_bundle(self.volume, blob.name, request) as impl: + impl['data']['blob'] = blob.name + return impl['guid'] + @route('DELETE', [None, None], acl=ACL.AUTH | ACL.AUTHOR) def delete(self, request): # Servers data should not be deleted immediately @@ -377,6 +391,8 @@ class NodeRoutes(db.Routes, model.Routes): if 'stability' not in request: request['stability'] = 'stable' + if 'layer' not in request: + request['layer'] = 'public' impls, __ = self.volume['implementation'].find( context=request.guid, order_by='-version', **request) @@ -392,6 +408,62 @@ class NodeRoutes(db.Routes, model.Routes): return impl +@contextmanager +def load_bundle(volume, bundle_path, impl=None): + if impl is None: + impl = {} + data = impl.setdefault('data', {}) + data['blob'] = bundle_path + + try: + bundle = Bundle(bundle_path, mime_type='application/zip') + except Exception: + _logger.debug('Load unrecognized bundle from %r', bundle_path) + context_type = 'content' + else: + _logger.debug('Load Sugar Activity bundle from %r', bundle_path) + context_type = 'activity' + unpack_size = 0 + with bundle: + for arcname in bundle.get_names(): + unpack_size += bundle.getmember(arcname).size + spec = bundle.get_spec() + extract = bundle.rootdir + if 'requires' in impl: + spec.requires.update(parse_requires(impl.pop('requires'))) + impl['context'] = spec['context'] + impl['version'] = spec['version'] + impl['stability'] = spec['stability'] + impl['license'] = spec['license'] + data['spec'] = {'*-*': { + 'commands': spec.commands, + 'requires': spec.requires, + 'extract': extract, + }} + data['unpack_size'] = unpack_size + data['mime_type'] = 'application/vnd.olpc-sugar' + + enforce('context' in impl, 'Context is not specified') + enforce('version' in impl, 'Version is not specified') + enforce(volume['context'].exists(impl['context']), + http.BadRequest, 'No such activity') + enforce(context_type in volume['context'].get(spec['context'])['type'], + http.BadRequest, 'Inappropriate bundle type') + if impl.get('license') in (None, EMPTY_LICENSE): + existing, total = volume['implementation'].find( + context=impl['context'], order_by='-version') + enforce(total, 'License is not specified') + impl['license'] = next(existing)['license'] + + yield impl + + existing, __ = volume['implementation'].find( + context=impl['context'], version=impl['version']) + for i in existing: + volume['implementation'].update(i.guid, {'layer': ['deleted']}) + impl['guid'] = volume['implementation'].create(impl) + + def _load_pubkey(pubkey): pubkey = pubkey.strip() try: diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index e02ceda..3638f47 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -174,6 +174,14 @@ class Client(object): if isinstance(dst, basestring): f.close() + def upload(self, path, data, **kwargs): + with file(data, 'rb') as f: + response = self.request('POST', path, f, params=kwargs) + if response.headers.get('Content-Type') == 'application/json': + return json.loads(response.content) + else: + return response.raw + def request(self, method, path=None, data=None, headers=None, allowed=None, params=None, **kwargs): if not path: diff --git a/tests/integration/node_client.py b/tests/integration/node_client.py index f7981c8..db37a74 100755 --- a/tests/integration/node_client.py +++ b/tests/integration/node_client.py @@ -6,7 +6,7 @@ import json import signal import shutil import zipfile -from os.path import exists, join +from os.path import exists, join, dirname, abspath from __init__ import tests, src_root @@ -36,6 +36,42 @@ class NodeClientTest(tests.Test): self.waitpid(self.client_pid, signal.SIGINT) tests.Test.tearDown(self) + def test_ReleaseActivity(self): + with file('bundle', 'wb') as f: + f.write(self.zips(['TestActivitry/activity/activity.info', [ + '[Activity]', + 'name = TestActivitry', + 'bundle_id = activity2', + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + 'stability = developper', + ]])) + self.cli(['release', 'bundle', '--porcelain']) + + self.assertEqual([ + {'version': '1', 'stability': 'developper', 'license': ['Public Domain']}, + ], + self.cli(['GET', '/implementation', 'context=activity2', 'reply=version,stability,license', 'order_by=version'])['result']) + + with file('bundle', 'wb') as f: + f.write(self.zips(['TestActivitry/activity/activity.info', [ + '[Activity]', + 'name = TestActivitry', + 'bundle_id = activity2', + 'exec = true', + 'icon = icon', + 'activity_version = 2', + ]])) + self.cli(['release', 'bundle', '--porcelain']) + + self.assertEqual([ + {'version': '1', 'stability': 'developper', 'license': ['Public Domain']}, + {'version': '2', 'stability': 'stable', 'license': ['Public Domain']}, + ], + self.cli(['GET', '/implementation', 'context=activity2', 'reply=version,stability,license', 'order_by=version'])['result']) + def test_CloneContext(self): context = self.cli(['POST', '/context'], stdin={ 'type': 'activity', @@ -43,31 +79,22 @@ class NodeClientTest(tests.Test): 'summary': 'summary', 'description': 'description', }) - impl = self.cli(['POST', '/implementation'], stdin={ - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - 'spec': { - '*-*': { - 'commands': { - 'activity': { - 'exec': 'true', - }, - }, - 'extract': 'topdir', - }, - }, - }) - bundle = zipfile.ZipFile('bundle', 'w') - bundle.writestr('/topdir/probe', 'ok') - bundle.close() - self.cli(['PUT', '/implementation/%s/data' % impl, '--post-file=bundle']) + + spec = ['[Activity]', + 'name = TestActivitry', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ] + with file('bundle', 'wb') as f: + f.write(self.zips(['TestActivitry/activity/activity.info', spec])) + impl = self.cli(['release', 'bundle']) self.cli(['PUT', '/context/%s' % context, 'cmd=clone', '-jd1']) - assert exists('client/Activities/topdir/probe') - self.assertEqual('ok', file('client/Activities/topdir/probe').read()) + assert exists('client/Activities/TestActivitry/activity/activity.info') + self.assertEqual('\n'.join(spec), file('client/Activities/TestActivitry/activity/activity.info').read()) def test_FavoriteContext(self): context = self.cli(['POST', '/context'], stdin={ diff --git a/tests/units/node/node.py b/tests/units/node/node.py index 85dcff3..65495f1 100755 --- a/tests/units/node/node.py +++ b/tests/units/node/node.py @@ -644,10 +644,10 @@ class NodeTest(tests.Test): 'stability = developer', 'requires = sugar>=0.88; dep' ]) - bundle = self.zips(('topdir/activity/activity.info', activity_info)) - guid = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'release'}).raw) + bundle1 = self.zips(('topdir/activity/activity.info', activity_info)) + guid1 = json.load(conn.request('POST', ['implementation'], bundle1, params={'cmd': 'release'}).raw) - impl = volume['implementation'].get(guid) + impl = volume['implementation'].get(guid1) self.assertEqual('bundle_id', impl['context']) self.assertEqual('1', impl['version']) self.assertEqual('developer', impl['stability']) @@ -655,9 +655,82 @@ class NodeTest(tests.Test): self.assertEqual('developer', impl['stability']) data = impl.meta('data') + self.assertEqual({ + '*-*': { + 'extract': 'topdir', + 'commands': {'activity': {'exec': 'true'}}, + 'requires': {'dep': {}, 'sugar': {'restrictions': [['0.88', None]]}}, + }, + }, + data['spec']) + self.assertEqual('application/vnd.olpc-sugar', data['mime_type']) - self.assertEqual(len(bundle), data['blob_size']) + self.assertEqual(len(bundle1), data['blob_size']) self.assertEqual(len(activity_info), data.get('unpack_size')) + self.assertEqual(bundle1, conn.get(['context', 'bundle_id'], cmd='clone', stability='developer')) + + activity_info = '\n'.join([ + '[Activity]', + 'name = TestActivitry', + 'bundle_id = bundle_id', + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + 'stability = stable', + ]) + bundle2 = self.zips(('topdir/activity/activity.info', activity_info)) + guid2 = json.load(conn.request('POST', ['implementation'], bundle2, params={'cmd': 'release'}).raw) + + self.assertEqual('1', volume['implementation'].get(guid1)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid1)['layer']) + self.assertEqual('2', volume['implementation'].get(guid2)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid2)['layer']) + self.assertEqual(bundle2, conn.get(['context', 'bundle_id'], cmd='clone')) + + activity_info = '\n'.join([ + '[Activity]', + 'name = TestActivitry', + 'bundle_id = bundle_id', + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + 'stability = stable', + ]) + bundle3 = self.zips(('topdir/activity/activity.info', activity_info)) + guid3 = json.load(conn.request('POST', ['implementation'], bundle3, params={'cmd': 'release'}).raw) + + self.assertEqual('1', volume['implementation'].get(guid1)['version']) + self.assertEqual(['deleted'], volume['implementation'].get(guid1)['layer']) + self.assertEqual('2', volume['implementation'].get(guid2)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid2)['layer']) + self.assertEqual('1', volume['implementation'].get(guid3)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid3)['layer']) + self.assertEqual(bundle2, conn.get(['context', 'bundle_id'], cmd='clone')) + + activity_info = '\n'.join([ + '[Activity]', + 'name = TestActivitry', + 'bundle_id = bundle_id', + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + 'stability = buggy', + ]) + bundle4 = self.zips(('topdir/activity/activity.info', activity_info)) + guid4 = json.load(conn.request('POST', ['implementation'], bundle4, params={'cmd': 'release'}).raw) + + self.assertEqual('1', volume['implementation'].get(guid1)['version']) + self.assertEqual(['deleted'], volume['implementation'].get(guid1)['layer']) + self.assertEqual('2', volume['implementation'].get(guid2)['version']) + self.assertEqual(['deleted'], volume['implementation'].get(guid2)['layer']) + self.assertEqual('1', volume['implementation'].get(guid3)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid3)['layer']) + self.assertEqual('2', volume['implementation'].get(guid4)['version']) + self.assertEqual(['public'], volume['implementation'].get(guid4)['layer']) + self.assertEqual(bundle3, conn.get(['context', 'bundle_id'], cmd='clone')) def call(routes, method, document=None, guid=None, prop=None, principal=None, cmd=None, content=None, **kwargs): |