Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2013-07-27 21:38:40 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2013-07-27 21:38:40 (GMT)
commitf68b009e2260ddf6d97eb0fa2ee7d15cf695aeea (patch)
tree92ba7d93a28b11cfe7c37964dc94fc243123d432
parentfa35499bcd89260b436c3c9f03c887661983c14d (diff)
Add release API command to easily upload new implementation
-rwxr-xr-xmisc/aslo-sync83
-rwxr-xr-xsugar-network149
-rw-r--r--sugar_network/client/cache.py9
-rw-r--r--sugar_network/model/implementation.py5
-rw-r--r--sugar_network/node/routes.py74
-rw-r--r--sugar_network/toolkit/http.py8
-rwxr-xr-xtests/integration/node_client.py75
-rwxr-xr-xtests/units/node/node.py81
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):