diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2014-03-24 11:55:25 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2014-03-24 11:55:25 (GMT) |
commit | 6ec16441c7c133c55385613f1e430c5ea37af632 (patch) | |
tree | 51870c8fa43a3bcabc6918206b3fc5265a91300a | |
parent | 40021927aa1815dd54e2e7839a46e5bd1ae8c7b3 (diff) |
Fix basic client routes
48 files changed, 1849 insertions, 3316 deletions
@@ -9,6 +9,7 @@ - changed pulls should take into account accept_length - secure node-to-node sync - cache init sync pull +- switch auth from WWW-AUTHENTICATE to mutual authentication over the HTTPS v2.0 ==== diff --git a/doc/objects.dia b/doc/objects.dia index 815102a..1d97f00 100644 --- a/doc/objects.dia +++ b/doc/objects.dia @@ -90,7 +90,7 @@ <dia:point val="30,26.3"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="17.68,25.55;30.05,30.85"/> + <dia:rectangle val="17.825,25.54;30.05,30.84"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -99,7 +99,7 @@ <dia:point val="30,26.3"/> <dia:point val="17.879,26.3"/> <dia:point val="17.879,30"/> - <dia:point val="18.265,30"/> + <dia:point val="18.41,30"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -484,10 +484,10 @@ <dia:string>#previews#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[blob] [R WA]#</dia:string> + <dia:string>#aggregated[blob] [R WA]#</dia:string> </dia:attribute> <dia:attribute name="value"> - <dia:string>##</dia:string> + <dia:string>#[]#</dia:string> </dia:attribute> <dia:attribute name="comment"> <dia:string>#List of screenshot in PNG and arbitrary sizes#</dia:string> @@ -507,7 +507,7 @@ <dia:string>#releases#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[] [R WA]#</dia:string> + <dia:string>#aggregated[] [R W]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>#[]#</dia:string> @@ -759,7 +759,7 @@ <dia:string>#pubkey#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#str [WN]#</dia:string> + <dia:string>#blob [WN]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>##</dia:string> @@ -789,7 +789,7 @@ <dia:point val="-13,6"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-13.015,5.985;-0.9725,24.415"/> + <dia:rectangle val="-13.015,5.985;-0.9725,27.215"/> </dia:attribute> <dia:attribute name="elem_corner"> <dia:point val="-13,6"/> @@ -798,7 +798,7 @@ <dia:real val="12.012499999999999"/> </dia:attribute> <dia:attribute name="elem_height"> - <dia:real val="18.400000000000002"/> + <dia:real val="21.200000000000003"/> </dia:attribute> <dia:attribute name="name"> <dia:string>#Resource#</dia:string> @@ -965,7 +965,7 @@ <dia:string>#author#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#list [R S F]#</dia:string> + <dia:string>#[] [R S F]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>##</dia:string> @@ -985,16 +985,16 @@ </dia:composite> <dia:composite type="umlattribute"> <dia:attribute name="name"> - <dia:string>#layer#</dia:string> + <dia:string>#tags#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[enum] [R WA]#</dia:string> + <dia:string>#[str] [R WA F]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>#[]#</dia:string> </dia:attribute> <dia:attribute name="comment"> - <dia:string>#List of layers this object belongs to; see the Wiki for details#</dia:string> + <dia:string>#List of tags set by the author#</dia:string> </dia:attribute> <dia:attribute name="visibility"> <dia:enum val="0"/> @@ -1008,16 +1008,39 @@ </dia:composite> <dia:composite type="umlattribute"> <dia:attribute name="name"> - <dia:string>#tags#</dia:string> + <dia:string>#status#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[str] [R WA F]#</dia:string> + <dia:string>#[enum] [R]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>#[]#</dia:string> </dia:attribute> <dia:attribute name="comment"> - <dia:string>#List of tags#</dia:string> + <dia:string>#Object status set by editors; see the Wiki for details#</dia:string> + </dia:attribute> + <dia:attribute name="visibility"> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="abstract"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="class_scope"> + <dia:boolean val="false"/> + </dia:attribute> + </dia:composite> + <dia:composite type="umlattribute"> + <dia:attribute name="name"> + <dia:string>#pins#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + <dia:string>#[enum] [R W]#</dia:string> + </dia:attribute> + <dia:attribute name="value"> + <dia:string>#[]#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + <dia:string>#List of preferences set by observer; see Wiki for details#</dia:string> </dia:attribute> <dia:attribute name="visibility"> <dia:enum val="0"/> @@ -1041,7 +1064,7 @@ <dia:point val="49.39,26.3"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="49.34,25.55;62.046,31.1085"/> + <dia:rectangle val="49.34,25.54;63.6285,31.0985"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -1050,7 +1073,7 @@ <dia:point val="49.39,26.3"/> <dia:point val="61.7373,26.3"/> <dia:point val="61.7373,30.2585"/> - <dia:point val="61.461,30.2585"/> + <dia:point val="63.5785,30.2585"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -1121,13 +1144,13 @@ <dia:point val="55.2435,30.2585"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="55.2285,30.2435;67.6935,64.6735"/> + <dia:rectangle val="55.2285,30.2435;71.9285,64.6735"/> </dia:attribute> <dia:attribute name="elem_corner"> <dia:point val="55.2435,30.2585"/> </dia:attribute> <dia:attribute name="elem_width"> - <dia:real val="12.434999999999999"/> + <dia:real val="16.669999999999998"/> </dia:attribute> <dia:attribute name="elem_height"> <dia:real val="34.400000000000006"/> @@ -1389,7 +1412,7 @@ <dia:string>#comments#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[str] [R W F I]#</dia:string> + <dia:string>#aggregated[str] [R W F I]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>#[]#</dia:string> @@ -1432,13 +1455,13 @@ </dia:composite> <dia:composite type="umlattribute"> <dia:attribute name="name"> - <dia:string>#data#</dia:string> + <dia:string>#attachments#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#blob [R WN]#</dia:string> + <dia:string>#aggregated[blob] [R WA]#</dia:string> </dia:attribute> <dia:attribute name="value"> - <dia:string>#null#</dia:string> + <dia:string>#[]#</dia:string> </dia:attribute> <dia:attribute name="comment"> <dia:string>#Attachments to the Post#</dia:string> @@ -1511,13 +1534,13 @@ <dia:point val="12,30"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="11.985,29.985;24.545,53.615"/> + <dia:rectangle val="11.985,29.985;24.835,53.615"/> </dia:attribute> <dia:attribute name="elem_corner"> <dia:point val="12,30"/> </dia:attribute> <dia:attribute name="elem_width"> - <dia:real val="12.529999999999999"/> + <dia:real val="12.82"/> </dia:attribute> <dia:attribute name="elem_height"> <dia:real val="23.600000000000005"/> @@ -1710,7 +1733,7 @@ <dia:string>#lsb_release#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#dict [R WN F]#</dia:string> + <dia:string>#{} [R WN F]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>##</dia:string> @@ -1756,7 +1779,7 @@ <dia:string>#logs#</dia:string> </dia:attribute> <dia:attribute name="type"> - <dia:string>#[blob] [R WN F]#</dia:string> + <dia:string>#aggregated[blob] [R WA F]#</dia:string> </dia:attribute> <dia:attribute name="value"> <dia:string>##</dia:string> diff --git a/sugar-network-client b/sugar-network-client index 9fffaf9..5b8b350 100755 --- a/sugar-network-client +++ b/sugar-network-client @@ -29,9 +29,10 @@ coroutine.inject() import sugar_network_webui as webui from sugar_network import db, toolkit, client, node from sugar_network.client.routes import CachedClientRoutes -from sugar_network.node import stats_user -from sugar_network.model import RESOURCES +from sugar_network.client.injector import Injector +from sugar_network.client.model import RESOURCES from sugar_network.toolkit.router import Router, Request, Response +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import mountpoints, printf, application from sugar_network.toolkit import Option @@ -100,9 +101,11 @@ class Application(application.Daemon): self.cmd_start() def run(self): + this.injector = Injector(client.path('cache'), + client.cache_lifetime.value, client.cache_limit.value, + client.cache_limit_percent.value) volume = db.Volume(client.path('db'), RESOURCES) - routes = CachedClientRoutes(volume, - client.api.value if not client.server_mode.value else None) + routes = CachedClientRoutes(volume) router = Router(routes, allow_spawn=True) logging.info('Listening for IPC requests on %s port', @@ -136,7 +139,8 @@ class Application(application.Daemon): if client.cache_timeout.value: self.jobs.spawn(self._recycle_cache, routes) - routes.connect() + api_url = None if client.discover_node.value else client.api.value + routes.connect(api_url) def delayed_start(event=None): for __ in routes.subscribe(event='delayed-start'): @@ -198,7 +202,6 @@ Option.seek('webui', webui) Option.seek('client', client) Option.seek('db', db) Option.seek('node', [node.port, node.find_limit]) -Option.seek('user-stats', stats_user) app = Application( name='sugar-network-client', diff --git a/sugar-network-node b/sugar-network-node index 7743390..b0b4425 100755 --- a/sugar-network-node +++ b/sugar-network-node @@ -24,7 +24,7 @@ from os.path import exists from sugar_network.toolkit import coroutine coroutine.inject() -from sugar_network import db, node, toolkit, model +from sugar_network import db, node, toolkit from sugar_network.node import obs, master, slave from sugar_network.toolkit.http import Connection from sugar_network.toolkit.router import Router @@ -54,13 +54,13 @@ class Application(application.Daemon): if node.certfile.value: ssl_args['certfile'] = node.certfile.value - if node.master_mode.value: + if node.mode.value == 'master': node_class = master.MasterRoutes resources = master.RESOURCES logging.info('Start master node') else: node_class = slave.SlaveRoutes - resources = model.RESOURCES + resources = slave.RESOURCES logging.info('Start slave node') volume = db.Volume(node.data_root.value, resources) cp = node_class(volume=volume, find_limit=node.find_limit.value) diff --git a/sugar-network-sync b/sugar-network-sync index 8d19813..dd37744 100755 --- a/sugar-network-sync +++ b/sugar-network-sync @@ -17,6 +17,8 @@ [ "${V}" ] && set -x +PARCEL_SUFFIX=".parcel" + info() { echo "-- $@" } @@ -45,18 +47,18 @@ Sugar Network sneakernet synchronization utility. Command arguments: PATH if specified, utility will try to recursively search for - synchronization packet files (files with ".sneakernet" suffix); + synchronization packet files (files with "$PARCEL_SUFFIX" suffix); using wget or curl utility, each file will be uploaded to the targeting Sugar Network server with downloading resulting packets; on success, uploaded packets will be removed and resulting packets will be placed to PATH instead URL if specified, should be Sugar Network API url, e.g., - http://api-testing.network.sugarlabs.org; script will download - full data dump from the server + http://node.sugarlabs.org; instead of synchronization, the script + will download full data dump from the specified master node Utility is intended to upload request packet files (generated by Sugar Network -node servers) to Sugar Network master server and download response packets -to deliver them back to nodes. +slave node) to Sugar Network master server and download response packets +to deliver them back to slave. See http://wiki.sugarlabs.org/go/Sugar_Network for details. EOF @@ -99,9 +101,14 @@ upload() { if [ $(stat -c %s "${out_packet}") -eq 0 ]; then rm "${out_packet}" else - out_filename="$(get_header_key "${out_packet}" filename)" - phase "Store results in ${out_filename}" - mv "${out_packet}" "${out_filename}" || abort "Cannot write ${out_filename}" + if [ "${in_packet}" ]; then + phase "Replace ${in_packet} with results" + mv "${out_packet}" "${in_packet}" || abort "Cannot write to ${in_packet}" + else + out_filename="$(get_hostname $url)"$PARCEL_SUFFIX + phase "Store results in ${out_filename}" + mv "${out_packet}" "${out_filename}" || abort "Cannot write to ${out_filename}" + fi fi fi @@ -174,13 +181,13 @@ if [ "${clone_url}" ]; then fi # Upload push packets at first -for package in $(find -type f -name '*.sneakernet' -printf '%f\n'); do - api_url="$(get_header_key "${package}" api_url)" +for parcel in $(find -type f -name "*$PARCEL_SUFFIX" -printf '%f\n'); do + api_url="$(get_header_key "${parcel}" api_url)" if [ -z "${api_url}" ]; then - info "Skip ${package}, it is not intended for uploading" + info "Skip ${parcel}, it is not intended for uploading" else - info "Push ${package} to ${api_url}" - upload "${api_url}?cmd=push" "$(get_hostname ${api_url}).cookie" "${package}" + info "Push ${parcel} to ${api_url}" + upload "${api_url}?cmd=push" "$(get_hostname ${api_url}).cookie" "${parcel}" fi done diff --git a/sugar_network/client/__init__.py b/sugar_network/client/__init__.py index 446795a..648d418 100644 --- a/sugar_network/client/__init__.py +++ b/sugar_network/client/__init__.py @@ -81,11 +81,6 @@ hub_root = Option( 'from file:// url', default='/usr/share/sugar-network/hub') -layers = Option( - 'comma separated list of layers to restrict Sugar Network content by', - default=[], type_cast=Option.list_cast, type_repr=Option.list_repr, - name='layers') - discover_node = Option( 'discover nodes in local network instead of using --api', default=False, type_cast=Option.bool_cast, @@ -179,7 +174,7 @@ def Connection(url=None, **args): def IPCConnection(): return http.Connection( - api='http://127.0.0.1:%s' % ipc_port.value, + 'http://127.0.0.1:%s' % ipc_port.value, # Online ipc->client->node request might fail if node connection # is lost in client process, so, re-send ipc request immediately # to retrive data from client in offline mode without propagating diff --git a/sugar_network/client/injector.py b/sugar_network/client/injector.py index 12baf51..6d0c420 100644 --- a/sugar_network/client/injector.py +++ b/sugar_network/client/injector.py @@ -27,6 +27,7 @@ from sugar_network import toolkit from sugar_network.client import packagekit, journal, profile_path from sugar_network.toolkit.spec import format_version from sugar_network.toolkit.bundle import Bundle +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import lsb_release, coroutine, i18n, pylru, http from sugar_network.toolkit import enforce @@ -47,6 +48,7 @@ class Injector(object): limit_bytes, limit_percent) self._api = None self._checkins = toolkit.Bin(join(root, 'checkins'), {}) + self._inprogress = {} for dir_name in ('solutions', 'releases'): dir_path = join(root, dir_name) @@ -81,8 +83,8 @@ class Injector(object): yield {'event': 'launch', 'state': 'init'} releases = [] - acquired = [] checkedin = {} + inprogress = [] environ = {} def acquire(ctx): @@ -92,8 +94,8 @@ class Injector(object): if ctx in self._checkins: checkedin[ctx] = (self.api, stability, self.seqno) else: - _logger.debug('Acquire %r', ctx) - acquired.extend(solution.values()) + inprogress.append((ctx, solution)) + self._progress_in(ctx) releases.extend(solution.values()) release = solution[ctx] return release, self._pool.path(release['blob']) @@ -135,9 +137,9 @@ class Injector(object): yield environ status = child.wait() finally: - if acquired: - _logger.debug('Release acquired contexts') - self._pool.push(acquired) + for ctx, solution in inprogress: + self._progress_out(ctx, True) + self._pool.push(solution.values()) if checkedin: with self._checkins as checkins: @@ -148,19 +150,22 @@ class Injector(object): yield {'event': 'launch', 'state': 'exit'} def checkin(self, context, stability='stable'): - if context in self._checkins: - _logger.debug('Refresh %r checkin', context) - else: - _logger.debug('Checkin %r', context) - yield {'event': 'checkin', 'state': 'solve'} - solution = self._solve(context, stability) - for event in self._download(solution.values()): - event['event'] = 'checkin' - yield event - self._pool.pop(solution.values()) - with self._checkins as checkins: - checkins[context] = (self.api, stability, self.seqno) - yield {'event': 'checkin', 'state': 'ready'} + self._progress_in(context) + try: + yield {'event': 'checkin', 'state': 'solve'} + solution = self._solve(context, stability) + for event in self._download(solution.values()): + event['event'] = 'checkin' + yield event + self._pool.pop(solution.values()) + with self._checkins as checkins: + checkins[context] = (self.api, stability, self.seqno) + yield {'event': 'checkin', 'state': 'ready'} + directory = this.volume['context'] + pins = list(set(directory[context]['pins']) | set(['checkin'])) + directory.update(context, {'pins': pins}) + finally: + self._progress_out(context) def checkout(self, context): if context not in self._checkins: @@ -171,8 +176,51 @@ class Injector(object): self._pool.push(solution.values()) with self._checkins as checkins: del checkins[context] + directory = this.volume['context'] + pins = list(set(directory[context]['pins']) - set(['checkin'])) + directory.update(context, {'pins': pins}) + self._notify(context) return True + def pins(self, context, stability='stable'): + result = [] + if self.api and context in self._checkins: + api, s, seqno = self._checkins[context] + if api != self.api or s != stability or seqno != self.seqno: + result.append('stale') + if self._inprogress.get(context): + result.append('inprogress') + return result + + def _notify(self, context, force=False): + if not force and not self.api: + return + doc = this.volume['context'][context] + pins = doc.repr('pins') if doc.exists else self.pins(context) + this.localcast({ + 'event': 'update', + 'resource': 'context', + 'guid': context, + 'props': {'pins': pins}, + }) + + def _progress_in(self, context): + progress = self._inprogress.setdefault(context, 0) + self._inprogress[context] = progress + 1 + if not progress: + _logger.debug('%r is in-progress', context) + self._notify(context, True) + + def _progress_out(self, context, force=False): + progress = self._inprogress.get(context) + if not progress: + _logger.warn('Progress counter broken for %r', context) + return + self._inprogress[context] = progress - 1 + if progress == 1: + _logger.debug('%r is not in-progress', context) + self._notify(context, force) + def _solve(self, context, stability): path = join(self._root, 'solutions', context) solution = None @@ -193,7 +241,8 @@ class Injector(object): _logger.debug('Reuse cached %r solution in offline', context) if not solution: - enforce(self.api, 'Cannot solve in offline') + enforce(self.api, http.ServiceUnavailable, + 'Not available in offline') _logger.debug('Solve %r', context) solution = self._api.get(['context', context], cmd='solve', stability=stability, lsb_id=lsb_release.distributor_id(), @@ -422,21 +471,21 @@ def _exec(context, release, path, args, environ): os.chdir(path) - environ = os.environ - environ['PATH'] = ':'.join([ + env = os.environ + env['PATH'] = ':'.join([ join(path, 'activity'), join(path, 'bin'), - environ['PATH'], + env['PATH'], ]) - environ['PYTHONPATH'] = path + ':' + environ.get('PYTHONPATH', '') - environ['SUGAR_BUNDLE_PATH'] = path - environ['SUGAR_BUNDLE_ID'] = context - environ['SUGAR_BUNDLE_NAME'] = i18n.decode(release['title']) - environ['SUGAR_BUNDLE_VERSION'] = format_version(release['version']) - environ['SUGAR_ACTIVITY_ROOT'] = datadir - environ['SUGAR_LOCALEDIR'] = join(path, 'locale') - - os.execvpe(args[0], args, environ) + env['PYTHONPATH'] = path + ':' + env.get('PYTHONPATH', '') + env['SUGAR_BUNDLE_PATH'] = path + env['SUGAR_BUNDLE_ID'] = context + env['SUGAR_BUNDLE_NAME'] = i18n.decode(release['title']) + env['SUGAR_BUNDLE_VERSION'] = format_version(release['version']) + env['SUGAR_ACTIVITY_ROOT'] = datadir + env['SUGAR_LOCALEDIR'] = join(path, 'locale') + + os.execvpe(args[0], args, env) except BaseException: logging.exception('Failed to execute %r args=%r', release, args) finally: diff --git a/sugar_network/client/journal.py b/sugar_network/client/journal.py index 0dcae12..6a8f5ed 100644 --- a/sugar_network/client/journal.py +++ b/sugar_network/client/journal.py @@ -19,8 +19,8 @@ import logging from shutil import copyfileobj from tempfile import NamedTemporaryFile -from sugar_network import client, toolkit -from sugar_network.toolkit.router import route, Request +from sugar_network import client +from sugar_network.toolkit.router import route, Request, File from sugar_network.toolkit import enforce @@ -105,14 +105,15 @@ class Routes(object): @route('GET', ['journal', None, 'preview']) def journal_get_preview(self, request, response): - return toolkit.File(_prop_path(request.guid, 'preview'), { - 'mime_type': 'image/png', + return File(_prop_path(request.guid, 'preview'), meta={ + 'content-type': 'image/png', }) @route('GET', ['journal', None, 'data']) def journal_get_data(self, request, response): - return toolkit.File(_ds_path(request.guid, 'data'), { - 'mime_type': get(request.guid, 'mime_type') or 'application/octet', + return File(_ds_path(request.guid, 'data'), meta={ + 'content-type': get(request.guid, 'mime_type') or + 'application/octet', }) @route('GET', ['journal', None, None], mime_type='application/json') diff --git a/sugar_network/client/model.py b/sugar_network/client/model.py new file mode 100644 index 0000000..6207af2 --- /dev/null +++ b/sugar_network/client/model.py @@ -0,0 +1,36 @@ +# Copyright (C) 2014 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from sugar_network import db +from sugar_network.model.user import User +from sugar_network.model.post import Post +from sugar_network.model.report import Report +from sugar_network.model import context as base_context +from sugar_network.toolkit.coroutine import this + + +_logger = logging.getLogger('client.model') + + +class Context(base_context.Context): + + @db.indexed_property(db.List, prefix='RP', default=[]) + def pins(self, value): + return value + this.injector.pins(self.guid) + + +RESOURCES = (User, Context, Post, Report) diff --git a/sugar_network/client/routes.py b/sugar_network/client/routes.py index 50d8632..c4b645d 100644 --- a/sugar_network/client/routes.py +++ b/sugar_network/client/routes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Aleksey Lim +# Copyright (C) 2012-2014 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 @@ -17,66 +17,53 @@ import os import logging from base64 import b64encode from httplib import IncompleteRead -from zipfile import ZipFile, ZIP_DEFLATED -from os.path import join, basename +from os.path import join from sugar_network import db, client, node, toolkit, model -from sugar_network.client import journal, releases -from sugar_network.node.slave import SlaveRoutes -from sugar_network.toolkit import netlink, mountpoints +from sugar_network.client import journal from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import ACL, Request, Response, Router from sugar_network.toolkit.router import route, fallbackroute -from sugar_network.toolkit import zeroconf, coroutine, http, exception, enforce +from sugar_network.toolkit import netlink, zeroconf, coroutine, http, parcel +from sugar_network.toolkit import lsb_release, exception, enforce -# Top-level directory name to keep SN data on mounted devices -_SN_DIRNAME = 'sugar-network' # Flag file to recognize a directory as a synchronization directory -_SYNC_DIRNAME = 'sugar-network-sync' _RECONNECT_TIMEOUT = 3 _RECONNECT_TIMEOUT_MAX = 60 * 15 -_LOCAL_LAYERS = frozenset(['local', 'clone', 'favorite']) _logger = logging.getLogger('client.routes') -class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): +class ClientRoutes(model.FrontRoutes, journal.Routes): - def __init__(self, home_volume, api_url=None, no_subscription=False): + def __init__(self, home_volume, no_subscription=False): model.FrontRoutes.__init__(self) - releases.Routes.__init__(self, home_volume) journal.Routes.__init__(self) + this.localcast = this.broadcast + self._local = _LocalRoutes(home_volume) self._inline = coroutine.Event() self._inline_job = coroutine.Pool() self._remote_urls = [] self._node = None - self._jobs = coroutine.Pool() + self._connect_jobs = coroutine.Pool() self._no_subscription = no_subscription - self._server_mode = not api_url - self._api_url = api_url self._auth = _Auth() - if not client.delayed_start.value: - self.connect() - - def connect(self): - self._got_offline(force=True) - if self._server_mode: - enforce(not client.login.value) - mountpoints.connect(_SN_DIRNAME, - self._found_mount, self._lost_mount) + def connect(self, api=None): + if self._connect_jobs: + return + self._got_offline() + if not api: + self._connect_jobs.spawn(self._discover_node) else: - if client.discover_server.value: - self._jobs.spawn(self._discover_node) - else: - self._remote_urls.append(self._api_url) - self._jobs.spawn(self._wait_for_connectivity) + self._remote_urls.append(api) + self._connect_jobs.spawn(self._wait_for_connectivity) def close(self): - self._jobs.kill() + self._connect_jobs.kill() self._got_offline() self._local.volume.close() @@ -132,63 +119,89 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): return result @route('GET', [None], - arguments={ - 'offset': int, - 'limit': int, - 'reply': ('guid',), - 'layer': list, - }, + arguments={'offset': int, 'limit': int, 'reply': ('guid',)}, mime_type='application/json') - def find(self, request, response, layer): - if set(request.get('layer', [])) & set(['favorite', 'clone']): + def find(self, request, response): + if not self._inline.is_set() or 'pins' in request: return self._local.call(request, response) reply = request.setdefault('reply', ['guid']) - if 'layer' not in reply: + if 'pins' not in reply: return self.fallback(request, response) if 'guid' not in reply: - # Otherwise there is no way to mixin local `layer` + # Otherwise there is no way to mixin `pins` reply.append('guid') result = self.fallback(request, response) directory = self._local.volume[request.resource] for item in result['result']: - if directory.exists(item['guid']): - existing_layer = directory.get(item['guid'])['layer'] - item['layer'][:] = set(item['layer']) | set(existing_layer) + doc = directory[item['guid']] + if doc.exists: + item['pins'] += doc.repr('pins') return result @route('GET', [None, None], mime_type='application/json') def get(self, request, response): - if self._local.volume[request.resource].exists(request.guid): + if self._local.volume[request.resource][request.guid].exists: return self._local.call(request, response) else: return self.fallback(request, response) @route('GET', [None, None, None], mime_type='application/json') def get_prop(self, request, response): - if self._local.volume[request.resource].exists(request.guid): + if self._local.volume[request.resource][request.guid].exists: return self._local.call(request, response) else: return self.fallback(request, response) @route('POST', ['report'], cmd='submit', mime_type='text/event-stream') - def submit_report(self, request, response): - logs = request.content.pop('logs') + def submit_report(self): + props = this.request.content + logs = props.pop('logs') + props['uname'] = os.uname() + props['lsb_release'] = { + 'distributor_id': lsb_release.distributor_id(), + 'release': lsb_release.release(), + } guid = self.fallback(method='POST', path=['report'], - content=request.content, content_type='application/json') - if logs: - with toolkit.TemporaryFile() as tmpfile: - with ZipFile(tmpfile, 'w', ZIP_DEFLATED) as zipfile: - for path in logs: - zipfile.write(path, basename(path)) - tmpfile.seek(0) - self.fallback(method='PUT', path=['report', guid, 'data'], - content_stream=tmpfile, content_type='application/zip') + content=props, content_type='application/json') + for logfile in logs: + with file(logfile) as f: + self.fallback(method='POST', path=['report', guid, 'logs'], + content_stream=f, content_type='text/plain') yield {'event': 'done', 'guid': guid} + @route('GET', ['context', None], cmd='launch', arguments={'args': list}, + mime_type='text/event-stream') + def launch(self): + return this.injector.launch(this.request.guid, **this.request) + + @route('PUT', ['context', None], cmd='checkin', + mime_type='text/event-stream') + def put_checkin(self): + self._checkin_context() + for event in this.injector.checkin(this.request.guid): + yield event + + @route('DELETE', ['context', None], cmd='checkin') + def delete_checkin(self, request): + this.injector.checkout(this.request.guid) + self._checkout_context() + + @route('PUT', ['context', None], cmd='favorite') + def put_favorite(self, request): + self._checkin_context('favorite') + + @route('DELETE', ['context', None], cmd='favorite') + def delete_favorite(self, request): + self._checkout_context('favorite') + + @route('GET', cmd='recycle') + def recycle(self): + return this.injector.recycle() + @fallbackroute() def fallback(self, request=None, response=None, **kwargs): if request is None: @@ -199,8 +212,6 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): if not self._inline.is_set(): return self._local.call(request, response) - if client.layers.value and request.resource in ('context', 'release'): - request.add('layer', *client.layers.value) request.principal = self._auth.login try: reply = self._node.call(request, response) @@ -217,21 +228,23 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): self._restart_online() return self._local.call(request, response) - def _got_online(self): + def _got_online(self, url): enforce(not self._inline.is_set()) _logger.debug('Got online on %r', self._node) self._inline.set() + self._local.volume.mute = True + this.injector.api = url this.localcast({'event': 'inline', 'state': 'online'}) - def _got_offline(self, force=False): - if not force and not self._inline.is_set(): - return + def _got_offline(self): if self._node is not None: self._node.close() if self._inline.is_set(): _logger.debug('Got offline on %r', self._node) - this.localcast({'event': 'inline', 'state': 'offline'}) self._inline.clear() + self._local.volume.mute = False + this.injector.api = None + this.localcast({'event': 'inline', 'state': 'offline'}) def _restart_online(self): _logger.debug('Lost %r connection, try to reconnect in %s seconds', @@ -256,10 +269,8 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): def pull_events(): for event in self._node.subscribe(): - if event.get('resource') == 'release': - mtime = event.get('mtime') - if mtime: - self.invalidate_solutions(mtime) + if event.get('event') == 'release': + this.injector.seqno = event['seqno'] this.broadcast(event) def handshake(url): @@ -267,16 +278,13 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): self._node = client.Connection(url, auth=self._auth) status = self._node.get(cmd='status') self._auth.allow_basic_auth = (status.get('level') == 'master') - """ - TODO switch to seqno - impl_info = status['resources'].get('release') - if impl_info: - self.invalidate_solutions(impl_info['mtime']) - """ + seqno = status.get('seqno') + if seqno and 'releases' in seqno: + this.injector.seqno = seqno['releases'] if self._inline.is_set(): _logger.info('Reconnected to %r node', url) else: - self._got_online() + self._got_online(url) def connect(): timeout = _RECONNECT_TIMEOUT @@ -307,40 +315,32 @@ class ClientRoutes(model.FrontRoutes, releases.Routes, journal.Routes): self._inline_job.kill() self._inline_job.spawn_later(timeout, connect) - def _found_mount(self, root): - if self._inline.is_set(): - _logger.debug('Found %r node mount but %r is already active', - root, self._node.volume.root) - return - - _logger.debug('Found %r node mount', root) - - db_path = join(root, _SN_DIRNAME, 'db') - node.data_root.value = db_path - node.stats_root.value = join(root, _SN_DIRNAME, 'stats') - node.files_root.value = join(root, _SN_DIRNAME, 'files') - volume = db.Volume(db_path, model.RESOURCES) - - if not volume['user'].exists(self._auth.login): - profile = self._auth.profile() - profile['guid'] = self._auth.login - volume['user'].create(profile) - - self._node = _NodeRoutes(join(db_path, 'node'), volume) - self._jobs.spawn(volume.populate) - - logging.info('Start %r node on %s port', volume.root, node.port.value) - server = coroutine.WSGIServer(('0.0.0.0', node.port.value), self._node) - self._inline_job.spawn(server.serve_forever) - self._got_online() - - def _lost_mount(self, root): - if not self._inline.is_set() or \ - not self._node.volume.root.startswith(root): + def _checkin_context(self, pin=None): + context = this.volume['context'][this.request.guid] + if not context.exists: + enforce(self.inline(), http.ServiceUnavailable, + 'Not available in offline') + _logger.debug('Checkin %r context', context.guid) + clone = self.fallback( + method='GET', path=['context', context.guid], cmd='clone') + this.volume.patch(next(parcel.decode(clone))) + pins = context['pins'] + if pin and pin not in pins: + this.volume['context'].update(context.guid, {'pins': pins + [pin]}) + + def _checkout_context(self, pin=None): + directory = this.volume['context'] + context = directory[this.request.guid] + if not context.exists: return - _logger.debug('Lost %r node mount', root) - self._inline_job.kill() - self._got_offline() + pins = set(context.repr('pins')) + if pin: + pins -= set([pin]) + if not self._inline.is_set() or pins: + if pin: + directory.update(context.guid, {'pins': list(pins)}) + else: + directory.delete(context.guid) class CachedClientRoutes(ClientRoutes): @@ -351,16 +351,16 @@ class CachedClientRoutes(ClientRoutes): self._push_job = coroutine.Pool() ClientRoutes.__init__(self, home_volume, api_url, no_subscription) - def _got_online(self): - ClientRoutes._got_online(self) + def _got_online(self, url): + ClientRoutes._got_online(self, url) self._push_job.spawn(self._push) - def _got_offline(self, force=True): + def _got_offline(self): self._push_job.kill() - ClientRoutes._got_offline(self, force) + ClientRoutes._got_offline(self) def _push(self): - # TODO should work using regular pull/push + # TODO should work using regular diff return @@ -384,14 +384,12 @@ class CachedClientRoutes(ClientRoutes): _logger.debug('Check %r local cache to push', res) - for guid, patch in volume[res].diff(self._push_seq, layer='local'): + for guid, patch in volume[res].diff(self._push_seq): diff = {} diff_seq = toolkit.Sequence() post_requests = [] for prop, meta, seqno in patch: value = meta['value'] - if prop == 'layer': - value = list(set(value) - _LOCAL_LAYERS) diff[prop] = value diff_seq.include(seqno, seqno) if not diff: @@ -436,67 +434,6 @@ class _LocalRoutes(db.Routes, Router): db.Routes.__init__(self, volume) Router.__init__(self, self) - def on_create(self, request, props): - props['layer'] = tuple(props['layer']) + ('local',) - db.Routes.on_create(self, request, props) - - -class _NodeRoutes(SlaveRoutes, Router): - - def __init__(self, key_path, volume): - SlaveRoutes.__init__(self, key_path, volume) - Router.__init__(self, self) - - self.api_url = 'http://127.0.0.1:%s' % node.port.value - self._mounts = toolkit.Pool() - self._jobs = coroutine.Pool() - - mountpoints.connect(_SYNC_DIRNAME, - self.__found_mountcb, self.__lost_mount_cb) - - def close(self): - self.volume.close() - - def __repr__(self): - return '<LocalNode path=%s api_url=%s>' % \ - (self.volume.root, self.api_url) - - def _sync_mounts(self): - this.localcast({'event': 'sync_start'}) - - for mountpoint in self._mounts: - this.localcast({'event': 'sync_next', 'path': mountpoint}) - try: - self._offline_session = self._offline_sync( - join(mountpoint, _SYNC_DIRNAME), - **(self._offline_session or {})) - except Exception, error: - _logger.exception('Failed to complete synchronization') - this.localcast({'event': 'sync_abort', 'error': str(error)}) - self._offline_session = None - raise - - if self._offline_session is None: - _logger.debug('Synchronization completed') - this.localcast({'event': 'sync_complete'}) - else: - _logger.debug('Postpone synchronization with %r session', - self._offline_session) - this.localcast({'event': 'sync_paused'}) - - def __found_mountcb(self, path): - self._mounts.add(path) - if self._jobs: - _logger.debug('Found %r sync mount, pool it', path) - else: - _logger.debug('Found %r sync mount, start synchronization', path) - self._jobs.spawn(self._sync_mounts) - - def __lost_mount_cb(self, path): - if self._mounts.remove(path) == toolkit.Pool.ACTIVE: - _logger.warning('%r was unmounted, break synchronization', path) - self._jobs.kill() - class _ResponseStream(object): diff --git a/sugar_network/db/directory.py b/sugar_network/db/directory.py index 9ebf907..7fe127d 100644 --- a/sugar_network/db/directory.py +++ b/sugar_network/db/directory.py @@ -20,8 +20,7 @@ from os.path import exists, join from sugar_network import toolkit from sugar_network.db.storage import Storage from sugar_network.db.metadata import Metadata, Guid -from sugar_network.toolkit.coroutine import this -from sugar_network.toolkit import http, exception, enforce +from sugar_network.toolkit import exception, enforce # To invalidate existed index on stcuture changes @@ -32,7 +31,7 @@ _logger = logging.getLogger('db.directory') class Directory(object): - def __init__(self, root, resource, index_class, seqno): + def __init__(self, root, resource, index_class, seqno, broadcast): """ :param index_class: what class to use to access to indexes, for regular casses @@ -52,6 +51,7 @@ class Directory(object): self._seqno = seqno self._storage = None self._index = None + self._broadcast = broadcast self._open() @@ -92,10 +92,10 @@ class Directory(object): guid = props['guid'] = toolkit.uuid() _logger.debug('Create %s[%s]: %r', self.metadata.name, guid, props) event = {'event': 'create', 'guid': guid} - self._index.store(guid, props, self._prestore, self._broadcast, event) + self._index.store(guid, props, self._prestore, self.broadcast, event) return guid - def update(self, guid, props): + def update(self, guid, props, event='update'): """Update properties for an existing document. :param guid: @@ -105,8 +105,10 @@ class Directory(object): """ _logger.debug('Update %s[%s]: %r', self.metadata.name, guid, props) - event = {'event': 'update', 'guid': guid} - self._index.store(guid, props, self._prestore, self._broadcast, event) + event = {'event': event, 'guid': guid} + if event['event'] == 'update': + event['props'] = props.copy() + self._index.store(guid, props, self._prestore, self.broadcast, event) def delete(self, guid): """Delete document. @@ -119,15 +121,9 @@ class Directory(object): event = {'event': 'delete', 'guid': guid} self._index.delete(guid, self._postdelete, guid, event) - def exists(self, guid): - return self._storage.get(guid).consistent - def get(self, guid): cached_props = self._index.get_cached(guid) record = self._storage.get(guid) - enforce(cached_props or record.exists, http.NotFound, - 'Resource %r does not exist in %r', - guid, self.metadata.name) return self.resource(guid, record, cached_props) def __getitem__(self, guid): @@ -202,10 +198,14 @@ class Directory(object): if doc.post_seqno is not None and doc.exists: # No need in after-merge event, further commit event # is enough to avoid increasing events flow - self._index.store(guid, doc.origs, self._preindex) + self._index.store(guid, doc.posts, self._preindex) return seqno + def broadcast(self, event): + event['resource'] = self.metadata.name + self._broadcast(event) + def _open(self): index_path = join(self._root, 'index', self.metadata.name) if self._is_layout_stale(): @@ -219,10 +219,6 @@ class Directory(object): self._storage = Storage(join(self._root, 'db', self.metadata.name)) _logger.debug('Open %r resource', self.resource) - def _broadcast(self, event): - event['resource'] = self.metadata.name - this.broadcast(event) - def _preindex(self, guid, changes): doc = self.resource(guid, self._storage.get(guid), changes) for prop in self.metadata: @@ -240,11 +236,11 @@ class Directory(object): def _postdelete(self, guid, event): self._storage.delete(guid) - self._broadcast(event) + self.broadcast(event) def _postcommit(self): self._seqno.commit() - self._broadcast({'event': 'commit', 'mtime': self._index.mtime}) + self.broadcast({'event': 'commit', 'mtime': self._index.mtime}) def _save_layout(self): path = join(self._root, 'index', self.metadata.name, 'layout') diff --git a/sugar_network/db/metadata.py b/sugar_network/db/metadata.py index 31cace1..67a6d13 100644 --- a/sugar_network/db/metadata.py +++ b/sugar_network/db/metadata.py @@ -374,6 +374,9 @@ class Aggregated(Composite): def subtypecast(self, value): return self._subtype.typecast(value) + def subreprcast(self, value): + return self._subtype.reprcast(value) + def subteardown(self, value): self._subtype.teardown(value) diff --git a/sugar_network/db/resource.py b/sugar_network/db/resource.py index d17637d..38c1ce4 100644 --- a/sugar_network/db/resource.py +++ b/sugar_network/db/resource.py @@ -13,14 +13,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import time + from sugar_network.db.metadata import indexed_property, Localized -from sugar_network.db.metadata import Numeric, List, Authors +from sugar_network.db.metadata import Numeric, List, Authors, Enum from sugar_network.db.metadata import Composite, Aggregated -from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import ACL from sugar_network.toolkit import ranges +STATES = ['active', 'deleted'] +STATUSES = ['featured'] + + class Resource(object): """Base class for all data classes.""" @@ -32,10 +37,13 @@ class Resource(object): def __init__(self, guid, record, origs=None, posts=None): self.origs = origs or {} self.posts = posts or {} - self.guid = guid - self.is_new = not bool(guid) self.record = record self._post_seqno = None + self._guid = guid + + @property + def guid(self): + return self._guid or self['guid'] @property def post_seqno(self): @@ -64,33 +72,34 @@ class Resource(object): def author(self, value): return value - @indexed_property(List, prefix='RL', default=[]) - def layer(self, value): - return value - - @layer.setter - def layer(self, value): - orig = self.orig('layer') - if 'deleted' in value: - if this.request.method != 'POST' and 'deleted' not in orig: - self.deleted() - elif this.request.method != 'POST' and 'deleted' in orig: - self.restored() + @indexed_property(Enum, STATES, prefix='RE', default=STATES[0], acl=0) + def state(self, value): return value @indexed_property(List, prefix='RT', full_text=True, default=[]) def tags(self, value): return value + @indexed_property(List, prefix='RU', default=[], acl=ACL.READ, + subtype=Enum(STATUSES)) + def status(self, value): + return value + + @indexed_property(List, prefix='RP', default=[]) + def pins(self, value): + return value + @property def exists(self): return self.record is not None and self.record.consistent - def deleted(self): - pass + def created(self): + ts = int(time.time()) + self.posts['ctime'] = ts + self.posts['mtime'] = ts - def restored(self): - pass + def updated(self): + self.posts['mtime'] = int(time.time()) def get(self, prop, default=None): """Get document's property value. @@ -128,6 +137,19 @@ class Resource(object): self.origs[prop.name] = value return value + def repr(self, prop): + """Get property value with applying output typecasts. + + Such property values should be used to return property + out from the system. + + """ + prop_ = self.metadata[prop] + value = prop_.reprcast(self.get(prop)) + if prop_.on_get is not None: + value = prop_.on_get(self, value) + return value + def properties(self, props): result = {} for i in props: diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py index e1f190c..f319658 100644 --- a/sugar_network/db/routes.py +++ b/sugar_network/db/routes.py @@ -14,7 +14,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import re -import time import logging from contextlib import contextmanager @@ -22,7 +21,7 @@ from sugar_network import toolkit from sugar_network.db.metadata import Aggregated from sugar_network.toolkit.router import ACL, File, route, fallbackroute from sugar_network.toolkit.coroutine import this -from sugar_network.toolkit import http, enforce +from sugar_network.toolkit import http, parcel, enforce _GUID_RE = re.compile('[a-zA-Z0-9_+-.]+$') @@ -40,20 +39,17 @@ class Routes(object): @route('POST', [None], acl=ACL.AUTH, mime_type='application/json') def create(self, request): with self._post(request, ACL.CREATE) as doc: - self.on_create(request, doc.posts) + doc.created() + if request.principal: + authors = doc.posts['author'] = {} + self._useradd(authors, request.principal, ACL.ORIGINAL) self.volume[request.resource].create(doc.posts) - self.after_post(doc) return doc['guid'] @route('GET', [None], - arguments={ - 'offset': int, - 'limit': int, - 'layer': [], - 'reply': ('guid',), - }, + arguments={'offset': int, 'limit': int, 'reply': ('guid',)}, mime_type='application/json') - def find(self, request, reply, limit, layer): + def find(self, request, reply, limit): self._preget(request) if self._find_limit: if limit <= 0: @@ -62,27 +58,22 @@ class Routes(object): _logger.warning('The find limit is restricted to %s', self._find_limit) request['limit'] = self._find_limit - if 'deleted' in layer: - _logger.warning('Requesting "deleted" layer, will ignore') - layer.remove('deleted') documents, total = self.volume[request.resource].find( - not_layer='deleted', **request) + not_state='deleted', **request) result = [self._postget(request, i, reply) for i in documents] return {'total': total, 'result': result} @route('GET', [None, None], cmd='exists', mime_type='application/json') def exists(self, request): - directory = self.volume[request.resource] - return directory.exists(request.guid) + return self.volume[request.resource][request.guid].exists @route('PUT', [None, None], acl=ACL.AUTH | ACL.AUTHOR) def update(self, request): with self._post(request, ACL.WRITE) as doc: if not doc.posts: return - self.on_update(request, doc.posts) + doc.updated() self.volume[request.resource].update(doc.guid, doc.posts) - self.after_post(doc) @route('PUT', [None, None, None], acl=ACL.AUTH | ACL.AUTHOR) def update_prop(self, request): @@ -97,8 +88,12 @@ class Routes(object): def delete(self, request): # Node data should not be deleted immediately # to make master-slave synchronization possible - request.content = {'layer': 'deleted'} - self.update(request) + directory = self.volume[request.resource] + doc = directory[request.guid] + enforce(doc.exists, http.NotFound, 'Resource not found') + doc.posts['state'] = 'deleted' + doc.updated() + directory.update(doc.guid, doc.posts, 'delete') @route('GET', [None, None], arguments={'reply': list}, mime_type='application/json') @@ -111,26 +106,16 @@ class Routes(object): reply.append(prop.name) self._preget(request) doc = self.volume[request.resource].get(request.guid) - enforce('deleted' not in doc['layer'], http.NotFound, 'Deleted') + enforce(doc.exists and doc['state'] != 'deleted', http.NotFound, + 'Resource not found') return self._postget(request, doc, reply) @route('GET', [None, None, None], mime_type='application/json') def get_prop(self, request, response): directory = self.volume[request.resource] - doc = directory.get(request.guid) - - prop = directory.metadata[request.prop] - prop.assert_access(ACL.READ) - - meta = doc.meta(prop.name) - if meta: - value = meta['value'] - response.last_modified = meta['mtime'] - else: - value = prop.default - value = _get_prop(doc, prop, value) + directory.metadata[request.prop].assert_access(ACL.READ) + value = directory[request.guid].repr(request.prop) enforce(value is not File.AWAY, http.NotFound, 'No blob') - return value @route('HEAD', [None, None, None]) @@ -152,6 +137,20 @@ class Routes(object): def remove_from_aggprop(self, request): self._aggpost(request, ACL.REMOVE, request.key) + @route('GET', [None, None, None, None], mime_type='application/json') + def get_aggprop(self): + doc = self.volume[this.request.resource][this.request.guid] + prop = doc.metadata[this.request.prop] + prop.assert_access(ACL.READ) + enforce(isinstance(prop, Aggregated), http.BadRequest, + 'Property is not aggregated') + agg_value = doc[prop.name].get(this.request.key) + enforce(agg_value is not None, http.NotFound, + 'Aggregated item not found') + value = prop.subreprcast(agg_value['value']) + enforce(value is not File.AWAY, http.NotFound, 'No blob') + return value + @route('PUT', [None, None], cmd='useradd', arguments={'role': 0}, acl=ACL.AUTH | ACL.AUTHOR) def useradd(self, request, user, role): @@ -171,60 +170,50 @@ class Routes(object): del authors[user] directory.update(request.guid, {'author': authors}) + @route('GET', [None, None], cmd='clone') + def clone(self, request): + clone = self.volume.clone(request.resource, request.guid) + return parcel.encode([('push', None, clone)]) + @fallbackroute('GET', ['blobs']) def blobs(self): return this.volume.blobs.get(this.request.guid) - def on_create(self, request, props): - ts = int(time.time()) - props['ctime'] = ts - props['mtime'] = ts - - if request.principal: - authors = props['author'] = {} - self._useradd(authors, request.principal, ACL.ORIGINAL) - - def on_update(self, request, props): - props['mtime'] = int(time.time()) - def on_aggprop_update(self, request, prop, value): pass - def after_post(self, doc): - pass - @contextmanager def _post(self, request, access): content = request.content enforce(isinstance(content, dict), http.BadRequest, 'Invalid value') - directory = self.volume[request.resource] if access == ACL.CREATE: - doc = directory.resource(None, None) if 'guid' in content: # TODO Temporal security hole, see TODO guid = content['guid'] - enforce(not directory.exists(guid), - http.BadRequest, '%s already exists', guid) enforce(_GUID_RE.match(guid) is not None, http.BadRequest, 'Malformed %s GUID', guid) else: - doc.posts['guid'] = toolkit.uuid() - for name, prop in directory.metadata.items(): + guid = toolkit.uuid() + doc = self.volume[request.resource][guid] + enforce(not doc.exists, 'Resource already exists') + doc.posts['guid'] = guid + for name, prop in doc.metadata.items(): if name not in content and prop.default is not None: doc.posts[name] = prop.default else: - doc = directory.get(request.guid) + doc = self.volume[request.resource][request.guid] + enforce(doc.exists, 'Resource not found') this.resource = doc def teardown(new, old): for name, value in new.items(): if old.get(name) != value: - directory.metadata[name].teardown(value) + doc.metadata[name].teardown(value) try: for name, value in content.items(): - prop = directory.metadata[name] + prop = doc.metadata[name] prop.assert_access(access, doc.orig(name)) if value is None: doc.posts[name] = prop.default @@ -255,8 +244,7 @@ class Routes(object): def _postget(self, request, doc, props): result = {} for name in props: - prop = doc.metadata[name] - value = _get_prop(doc, prop, doc.get(name)) + value = doc.repr(name) if isinstance(value, File): value = value.url result[name] = value @@ -264,10 +252,9 @@ class Routes(object): def _useradd(self, authors, user, role): props = {} - - users = self.volume['user'] - if users.exists(user): - props['name'] = users.get(user)['name'] + user_doc = self.volume['user'][user] + if user_doc.exists: + props['name'] = user_doc['name'] role |= ACL.INSYSTEM else: role &= ~ACL.INSYSTEM @@ -316,15 +303,8 @@ class Routes(object): authors = aggvalue['author'] = {} role = ACL.ORIGINAL if request.principal in doc['author'] else 0 self._useradd(authors, request.principal, role) - props = {request.prop: {aggid: aggvalue}} - self.on_update(request, props) - self.volume[request.resource].update(request.guid, props) + doc.posts[request.prop] = {aggid: aggvalue} + doc.updated() + self.volume[request.resource].update(request.guid, doc.posts) return aggid - - -def _get_prop(doc, prop, value): - value = prop.reprcast(value) - if prop.on_get is not None: - value = prop.on_get(doc, value) - return value diff --git a/sugar_network/db/volume.py b/sugar_network/db/volume.py index 5d9bac1..7bf738c 100644 --- a/sugar_network/db/volume.py +++ b/sugar_network/db/volume.py @@ -19,9 +19,11 @@ from copy import deepcopy from os.path import exists, join, abspath from sugar_network import toolkit +from sugar_network.db.metadata import Blob from sugar_network.db.directory import Directory from sugar_network.db.index import IndexWriter from sugar_network.db.blobs import Blobs +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, coroutine, ranges, enforce @@ -35,6 +37,7 @@ class Volume(dict): def __init__(self, root, documents, index_class=None): Volume._flush_pool.append(self) self.resources = {} + self.mute = False self._populators = coroutine.Pool() if index_class is None: @@ -122,6 +125,17 @@ class Volume(dict): ranges.exclude(r, None, last_seqno) yield {'commit': commit_r} + def clone(self, resource, guid): + doc = self[resource][guid] + patch = doc.diff([[1, None]]) + if not patch: + return + for name, prop in self[resource].metadata.items(): + if isinstance(prop, Blob) and name in patch: + yield self.blobs.get(patch[name]['value']) + yield {'resource': resource} + yield {'guid': guid, 'patch': patch} + def patch(self, records): directory = None committed = [] @@ -150,6 +164,13 @@ class Volume(dict): return seqno, committed + def broadcast(self, event): + if not self.mute: + if event['event'] == 'commit': + this.broadcast(event) + else: + this.localcast(event) + def __enter__(self): return self @@ -167,7 +188,8 @@ class Volume(dict): cls = getattr(mod, name.capitalize()) else: cls = resource - dir_ = Directory(self._root, cls, self._index_class, self.seqno) + dir_ = Directory(self._root, cls, self._index_class, self.seqno, + self.broadcast) self._populators.spawn(self._populate, dir_) self[name] = dir_ return dir_ diff --git a/sugar_network/model/__init__.py b/sugar_network/model/__init__.py index 9e1aaf5..4ff89ff 100644 --- a/sugar_network/model/__init__.py +++ b/sugar_network/model/__init__.py @@ -122,7 +122,6 @@ def generate_node_stats(volume): def load_bundle(blob, context=None, initial=False, extra_deps=None): - contexts = this.volume['context'] context_type = None context_meta = None release_notes = None @@ -186,18 +185,21 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): enforce(context, http.BadRequest, 'Context is not specified') enforce(version, http.BadRequest, 'Version is not specified') release['version'] = parse_version(version) - if initial and not contexts.exists(context): - enforce(context_meta, http.BadRequest, 'No way to initate context') - context_meta['guid'] = context - context_meta['type'] = [context_type] - this.call(method='POST', path=['context'], content=context_meta) + + doc = this.volume['context'][context] + if initial: + if not doc.exists: + enforce(context_meta, http.BadRequest, 'No way to initate context') + context_meta['guid'] = context + context_meta['type'] = [context_type] + this.call(method='POST', path=['context'], content=context_meta) else: - enforce(context_type in contexts[context]['type'], + enforce(doc.exists, http.NotFound, 'No context') + enforce(context_type in doc['type'], http.BadRequest, 'Inappropriate bundle type') - context_doc = contexts[context] if 'license' not in release: - releases = context_doc['releases'].values() + releases = doc['releases'].values() enforce(releases, http.BadRequest, 'License is not specified') recent = max(releases, key=lambda x: x.get('value', {}).get('release')) enforce(recent, http.BadRequest, 'License is not specified') @@ -205,11 +207,11 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): _logger.debug('Load %r release: %r', context, release) - if this.request.principal in context_doc['author']: - patch = context_doc.format_patch(context_meta) + if this.request.principal in doc['author']: + patch = doc.format_patch(context_meta) if patch: this.call(method='PUT', path=['context', context], content=patch) - context_doc.posts.update(patch) + doc.posts.update(patch) # TRANS: Release notes title title = i18n._('%(name)s %(version)s release') else: @@ -220,7 +222,7 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): 'context': context, 'type': 'notification', 'title': i18n.encode(title, - name=context_doc['title'], + name=doc['title'], version=version, ), 'message': release_notes or '', @@ -228,7 +230,7 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): content_type='application/json') blob['content-disposition'] = 'attachment; filename="%s-%s%s"' % ( - ''.join(i18n.decode(context_doc['title']).split()), + ''.join(i18n.decode(doc['title']).split()), version, mimetypes.guess_extension(blob.get('content-type')) or '', ) this.volume.blobs.update(blob.digest, blob) diff --git a/sugar_network/model/context.py b/sugar_network/model/context.py index 3aceacc..5e12360 100644 --- a/sugar_network/model/context.py +++ b/sugar_network/model/context.py @@ -21,7 +21,11 @@ from sugar_network.toolkit import svg_to_png class Context(db.Resource): - @db.indexed_property(db.List, prefix='T', full_text=True, + @db.indexed_property(db.List, prefix='P', default=[]) + def pins(self, value): + return value + + @db.indexed_property(db.List, prefix='T', subtype=db.Enum(model.CONTEXT_TYPES)) def type(self, value): return value @@ -74,7 +78,7 @@ class Context(db.Resource): def homepage(self, value): return value - @db.indexed_property(db.List, prefix='Y', default=[], full_text=True) + @db.indexed_property(db.List, prefix='Y', default=[]) def mime_types(self, value): return value @@ -90,7 +94,8 @@ class Context(db.Resource): def logo(self, value): return value - @db.stored_property(db.Aggregated, subtype=db.Blob()) + @db.stored_property(db.Aggregated, subtype=db.Blob(), + acl=ACL.READ | ACL.INSERT | ACL.REMOVE | ACL.AUTHOR) def previews(self, value): return value @@ -99,12 +104,6 @@ class Context(db.Resource): def releases(self, value): return value - @releases.setter - def releases(self, value): - if value or this.request.method != 'POST': - self.invalidate_solutions() - return value - @db.indexed_property(db.Numeric, slot=2, default=0, acl=ACL.READ | ACL.CALC) def downloads(self, value): @@ -124,20 +123,19 @@ class Context(db.Resource): """ return value - @dependencies.setter - def dependencies(self, value): - if value or this.request.method != 'POST': - self.invalidate_solutions() - return value - - def deleted(self): - self.invalidate_solutions() + def created(self): + db.Resource.created(self) + self._invalidate_solutions() - def restored(self): - self.invalidate_solutions() + def updated(self): + db.Resource.updated(self) + self._invalidate_solutions() - def invalidate_solutions(self): - this.broadcast({ - 'event': 'release', - 'seqno': this.volume.releases_seqno.next(), - }) + def _invalidate_solutions(self): + if self['releases'] and \ + [i for i in ('state', 'releases', 'dependencies') + if i in self.posts and self.posts[i] != self.orig(i)]: + this.broadcast({ + 'event': 'release', + 'seqno': this.volume.releases_seqno.next(), + }) diff --git a/sugar_network/model/post.py b/sugar_network/model/post.py index 21046f2..d924617 100644 --- a/sugar_network/model/post.py +++ b/sugar_network/model/post.py @@ -74,7 +74,8 @@ class Post(db.Resource): def preview(self, value): return value - @db.stored_property(db.Aggregated, subtype=db.Blob()) + @db.stored_property(db.Aggregated, subtype=db.Blob(), + acl=ACL.READ | ACL.INSERT | ACL.REMOVE | ACL.AUTHOR) def attachments(self, value): if value: value['name'] = self['title'] diff --git a/sugar_network/model/report.py b/sugar_network/model/report.py index be9fd9f..a434a6d 100644 --- a/sugar_network/model/report.py +++ b/sugar_network/model/report.py @@ -60,6 +60,7 @@ class Report(db.Resource): def solution(self, value): return value - @db.stored_property(db.Aggregated, subtype=db.Blob()) + @db.stored_property(db.Aggregated, subtype=db.Blob(), + acl=ACL.READ | ACL.INSERT | ACL.AUTHOR) def logs(self, value): return value diff --git a/sugar_network/model/routes.py b/sugar_network/model/routes.py index af19023..fb409d4 100644 --- a/sugar_network/model/routes.py +++ b/sugar_network/model/routes.py @@ -28,6 +28,7 @@ class FrontRoutes(object): def __init__(self): self._spooler = coroutine.Spooler() this.broadcast = self._broadcast + this.localcast = self._broadcast @route('GET', mime_type='text/html') def hello(self): diff --git a/sugar_network/node/master.py b/sugar_network/node/master.py index 61d32fb..b93dcbc 100644 --- a/sugar_network/node/master.py +++ b/sugar_network/node/master.py @@ -17,6 +17,9 @@ import logging from urlparse import urlsplit from sugar_network import toolkit +from sugar_network.model.post import Post +from sugar_network.model.report import Report +from sugar_network.node.model import User, Context from sugar_network.node import obs, master_api from sugar_network.node.routes import NodeRoutes from sugar_network.toolkit.router import route, ACL @@ -24,12 +27,7 @@ from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, parcel, pylru, ranges, enforce -RESOURCES = ( - 'sugar_network.node.model', - 'sugar_network.model.post', - 'sugar_network.model.report', - 'sugar_network.model.user', - ) +RESOURCES = (User, Context, Post, Report) _logger = logging.getLogger('node.master') diff --git a/sugar_network/node/model.py b/sugar_network/node/model.py index 8de6038..8f9819b 100644 --- a/sugar_network/node/model.py +++ b/sugar_network/node/model.py @@ -14,10 +14,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import bisect +import hashlib import logging from sugar_network import db -from sugar_network.model import Release, context as base_context +from sugar_network.model import Release, context as _context, user as _user + from sugar_network.node import obs from sugar_network.toolkit.router import ACL from sugar_network.toolkit.coroutine import this @@ -28,6 +30,13 @@ _logger = logging.getLogger('node.model') _presolve_queue = None +class User(_user.User): + + def created(self): + with file(this.volume.blobs.get(self['pubkey']).path) as f: + self.posts['guid'] = str(hashlib.sha1(f.read()).hexdigest()) + + class _Release(Release): _package_cast = db.Dict(db.List()) @@ -87,23 +96,17 @@ class _Release(Release): def teardown(self, value): if 'package' not in this.resource['type']: - return Release.typecast(self, value) + return Release.teardown(self, value) # TODO Delete presolved files -class Context(base_context.Context): +class Context(_context.Context): @db.stored_property(db.Aggregated, subtype=_Release(), acl=ACL.READ | ACL.INSERT | ACL.REMOVE | ACL.REPLACE) def releases(self, value): return value - @releases.setter - def releases(self, value): - if value or this.request.method != 'POST': - self.invalidate_solutions() - return value - def solve(volume, top_context, command=None, lsb_id=None, lsb_release=None, stability=None, requires=None): @@ -151,6 +154,7 @@ def solve(volume, top_context, command=None, lsb_id=None, lsb_release=None, if context in context_clauses: return context_clauses[context] context = volume['context'][context] + enforce(context.exists, http.NotFound, 'Context not found') releases = context['releases'] clause = [] diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index 5fdb27e..ea23297 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -121,9 +121,9 @@ class NodeRoutes(db.Routes, FrontRoutes): enforce(solution is not None, 'Failed to solve') return solution - @route('GET', ['context', None], cmd='clone', - arguments={'requires': list}) - def get_clone(self, request, response): + @route('GET', ['context', None], cmd='resolve', + arguments={'requires': list, 'stability': list}) + def resolve(self, request): solution = self.solve(request) return this.volume.blobs.get(solution[request.guid]['blob']) @@ -149,12 +149,6 @@ class NodeRoutes(db.Routes, FrontRoutes): enforce(self.authorize(request.principal, 'root'), http.Forbidden, 'Operation is permitted only for superusers') - def on_create(self, request, props): - if request.resource == 'user': - with file(this.volume.blobs.get(props['pubkey']).path) as f: - props['guid'] = str(hashlib.sha1(f.read()).hexdigest()) - db.Routes.on_create(self, request, props) - def on_aggprop_update(self, request, prop, value): if prop.acl & ACL.AUTHOR: self._enforce_authority(request) @@ -164,8 +158,8 @@ class NodeRoutes(db.Routes, FrontRoutes): def authenticate(self, auth): enforce(auth.scheme == 'sugar', http.BadRequest, 'Unknown authentication scheme') - if not self.volume['user'].exists(auth.login): - raise Unauthorized('Principal does not exist', auth.nonce) + enforce(self.volume['user'][auth.login].exists, Unauthorized, + 'Principal does not exist') from M2Crypto import RSA diff --git a/sugar_network/node/slave.py b/sugar_network/node/slave.py index 333e6ea..76593e9 100644 --- a/sugar_network/node/slave.py +++ b/sugar_network/node/slave.py @@ -22,6 +22,10 @@ from os.path import join, dirname, exists, isabs from gettext import gettext as _ from sugar_network import toolkit +from sugar_network.model.context import Context +from sugar_network.model.post import Post +from sugar_network.model.report import Report +from sugar_network.node.model import User from sugar_network.node import master_api from sugar_network.node.routes import NodeRoutes from sugar_network.toolkit.router import route, ACL @@ -29,6 +33,8 @@ from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, parcel, ranges, enforce +RESOURCES = (User, Context, Post, Report) + _logger = logging.getLogger('node.slave') @@ -46,7 +52,6 @@ class SlaveRoutes(NodeRoutes): @route('POST', cmd='online_sync', acl=ACL.LOCAL, arguments={'no_pull': bool}) def online_sync(self, no_pull=False): - self._export(not no_pull) conn = http.Connection(master_api.value) response = conn.request('POST', data=parcel.encode(self._export(not no_pull), header={ @@ -100,22 +105,22 @@ class SlaveRoutes(NodeRoutes): seqno, committed = this.volume.patch(packet) if seqno is not None: if from_master: - ranges.exclude(self._pull_r.value, committed) - self._pull_r.commit() + with self._pull_r as r: + ranges.exclude(r, committed) else: requests.append(('request', { 'origin': sender, 'ranges': committed, }, [])) - ranges.exclude(self._push_r.value, seqno, seqno) - self._push_r.commit() + with self._push_r as r: + ranges.exclude(r, seqno, seqno) elif packet.name == 'ack' and from_master and \ packet['to'] == self.guid: - ranges.exclude(self._pull_r.value, packet['ack']) - self._pull_r.commit() + with self._pull_r as r: + ranges.exclude(r, packet['ack']) if packet['ranges']: - ranges.exclude(self._push_r.value, packet['ranges']) - self._push_r.commit() + with self._push_r as r: + ranges.exclude(r, packet['ranges']) return requests diff --git a/sugar_network/toolkit/coroutine.py b/sugar_network/toolkit/coroutine.py index b43c1e9..4a54975 100644 --- a/sugar_network/toolkit/coroutine.py +++ b/sugar_network/toolkit/coroutine.py @@ -370,8 +370,9 @@ def _print_exception(context, klass, value, tb): context = 'Undefined' elif not isinstance(context, basestring): if isinstance(context, dict) and 'PATH_INFO' in context: - context_repr = '%s%s' % \ - (context['PATH_INFO'], context.get('QUERY_STRING') or '') + context_repr = context['PATH_INFO'] + if 'QUERY_STRING' in context: + context_repr += '?' + context['QUERY_STRING'] try: context = self.format_context(context) except Exception: diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 9b9754e..9dd437e 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -202,6 +202,9 @@ class Connection(object): if not isinstance(path, basestring): path = '/'.join([i.strip('/') for i in [self.url] + path]) + # TODO Disable cookies on requests library level + self._session.cookies.clear() + try_ = 0 while True: try_ += 1 diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 8e23863..8eb84da 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -602,11 +602,11 @@ class Router(object): request.ensure_content() coroutine.spawn(self._event_stream, request, result) result = None + elif route_.mime_type and 'content-type' not in response: + response.set('content-type', route_.mime_type) except Exception, exception: + # To populate `exception` only raise - else: - if route_.mime_type and 'content-type' not in response: - response.set('content-type', route_.mime_type) finally: for i in self._postroutes: i(request, response, result, exception) @@ -689,7 +689,8 @@ class Router(object): elif not streamed_content: if response.content_type == 'application/json': content = json.dumps(content) - if 'content-length' not in response: + response.content_length = len(content) + elif 'content-length' not in response: response.content_length = len(content) if content else 0 if request.method == 'HEAD' and content is not None: _logger.warning('Content from HEAD response is ignored') @@ -753,21 +754,12 @@ class Router(object): commons['guid'] = request.guid if request.prop: commons['prop'] = request.prop - try: - for event in _event_stream(request, stream): - if 'event' not in event: - commons.update(event) - else: - event.update(commons) - this.localcast(event) - except Exception, error: - _logger.exception('Event stream %r failed', request) - event = {'event': 'failure', - 'exception': type(error).__name__, - 'error': str(error), - } - event.update(commons) - this.localcast(event) + for event in _event_stream(request, stream): + if 'event' not in event: + commons.update(event) + else: + event.update(commons) + this.localcast(event) def _assert_origin(self, environ): origin = environ['HTTP_ORIGIN'] @@ -837,6 +829,12 @@ def _event_stream(request, stream): event[0].update(i) event = event[0] yield event + except Exception, error: + _logger.exception('Event stream %r failed', request) + yield {'event': 'failure', + 'exception': type(error).__name__, + 'error': str(error), + } finally: _logger.debug('Event stream %r exited', request) diff --git a/tests/__init__.py b/tests/__init__.py index 1f5118c..386616a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -22,14 +22,17 @@ coroutine.inject() from sugar_network.toolkit import http, mountpoints, Option, gbus, i18n, languages, parcel from sugar_network.toolkit.router import Router, Request from sugar_network.toolkit.coroutine import this -#from sugar_network.client import IPCConnection, journal, routes as client_routes -#from sugar_network.client.routes import ClientRoutes, _Auth +from sugar_network.client import IPCConnection, journal, routes as client_routes, model as client_model +from sugar_network.client.injector import Injector +from sugar_network.client.routes import ClientRoutes, _Auth from sugar_network import db, client, node, toolkit, model from sugar_network.model.user import User from sugar_network.model.context import Context +from sugar_network.node.model import Context as MasterContext +from sugar_network.node.model import User as MasterUser from sugar_network.model.post import Post from sugar_network.node.master import MasterRoutes -from sugar_network.node import obs, slave +from sugar_network.node import obs, slave, master from requests import adapters @@ -91,13 +94,12 @@ class Test(unittest.TestCase): client.api.value = 'http://127.0.0.1:7777' client.mounts_root.value = None client.ipc_port.value = 5555 - client.layers.value = None client.cache_limit.value = 0 client.cache_limit_percent.value = 0 client.cache_lifetime.value = 0 client.keyfile.value = join(root, 'data', UID) - #client_routes._RECONNECT_TIMEOUT = 0 - #journal._ds_root = tmpdir + '/datastore' + client_routes._RECONNECT_TIMEOUT = 0 + journal._ds_root = tmpdir + '/datastore' mountpoints._connects.clear() mountpoints._found.clear() mountpoints._COMPLETE_MOUNT_TIMEOUT = .1 @@ -114,11 +116,11 @@ class Test(unittest.TestCase): 'sugar_network.model.report', ] - #if tmp_root is None: - # self.override(_Auth, 'profile', lambda self: { - # 'name': 'test', - # 'pubkey': PUBKEY, - # }) + if tmp_root is None: + self.override(_Auth, 'profile', lambda self: { + 'name': 'test', + 'pubkey': PUBKEY, + }) os.makedirs('tmp') @@ -131,6 +133,7 @@ class Test(unittest.TestCase): this.volume = None this.call = None this.broadcast = lambda x: x + this.injector = None def tearDown(self): self.stop_nodes() @@ -153,6 +156,11 @@ class Test(unittest.TestCase): self.assertEqual(0, self.waitpid(pid)) coroutine.shutdown() + def stop_master(self): + while self.forks: + pid = self.forks.pop() + self.assertEqual(0, self.waitpid(pid)) + def waitpid(self, pid, sig=signal.SIGTERM, ignore_status=False): if pid in self.forks: self.forks.remove(pid) @@ -258,7 +266,7 @@ class Test(unittest.TestCase): def start_master(self, classes=None, routes=MasterRoutes): if classes is None: - classes = [User, Context, Post] + classes = master.RESOURCES #self.touch(('master/etc/private/node', file(join(root, 'data', NODE_UID)).read())) self.node_volume = db.Volume('master', classes) self.node_routes = routes(volume=self.node_volume) @@ -272,7 +280,7 @@ class Test(unittest.TestCase): def fork_master(self, classes=None, routes=MasterRoutes): if classes is None: - classes = [User, Context] + classes = master.RESOURCES def node(): volume = db.Volume('master', classes) @@ -284,12 +292,11 @@ class Test(unittest.TestCase): return pid def start_client(self, classes=None, routes=None): - if classes is None: - classes = [User, Context] if routes is None: routes = ClientRoutes - volume = db.Volume('client', classes) - self.client_routes = routes(volume, client.api.value) + volume = db.Volume('client', classes or client_model.RESOURCES) + self.client_routes = routes(volume) + self.client_routes.connect(client.api.value) self.client = coroutine.WSGIServer( ('127.0.0.1', client.ipc_port.value), Router(self.client_routes)) coroutine.spawn(self.client.serve_forever) @@ -298,29 +305,34 @@ class Test(unittest.TestCase): return volume def start_online_client(self, classes=None): - if classes is None: - classes = [User, Context] - self.start_master(classes) - volume = db.Volume('client', classes) - self.client_routes = ClientRoutes(volume, client.api.value) + self.fork_master(classes) + this.injector = Injector('client/cache') + home_volume = db.Volume('client', classes or client_model.RESOURCES) + self.client_routes = ClientRoutes(home_volume) + self.client_routes.connect(client.api.value) self.wait_for_events(self.client_routes, event='inline', state='online').wait() self.client = coroutine.WSGIServer( ('127.0.0.1', client.ipc_port.value), Router(self.client_routes)) coroutine.spawn(self.client.serve_forever) coroutine.dispatch() - this.volume = volume - return volume + this.volume = home_volume + return home_volume def start_offline_client(self, resources=None): - self.home_volume = db.Volume('db', resources or model.RESOURCES) - self.client_routes = ClientRoutes(self.home_volume) + this.injector = Injector('client/cache') + home_volume = db.Volume('client', resources or client_model.RESOURCES) + self.client_routes = ClientRoutes(home_volume) server = coroutine.WSGIServer(('127.0.0.1', client.ipc_port.value), Router(self.client_routes)) coroutine.spawn(server.serve_forever) coroutine.dispatch() - this.volume = self.home_volume - return IPCConnection() - - def wait_for_events(self, cp, **condition): + this.volume = home_volume + return home_volume + + def wait_for_events(self, cp=None, **condition): + if cp is None: + cp = self.client_routes + if hasattr(cp, 'inline') and not cp.inline(): + cp.connect(client.api.value) trigger = coroutine.AsyncResult() def waiter(trigger): diff --git a/tests/units/client/__main__.py b/tests/units/client/__main__.py index 5d48161..fcc26a6 100644 --- a/tests/units/client/__main__.py +++ b/tests/units/client/__main__.py @@ -4,9 +4,6 @@ from __init__ import tests from journal import * from routes import * -from offline_routes import * -from online_routes import * -from server_routes import * from injector import * from packagekit import * diff --git a/tests/units/client/injector.py b/tests/units/client/injector.py index 4b12fe2..ec88975 100755 --- a/tests/units/client/injector.py +++ b/tests/units/client/injector.py @@ -12,8 +12,9 @@ from os.path import exists, join, basename from __init__ import tests from sugar_network import db, client -from sugar_network.client import Connection, keyfile, api, packagekit, injector as injector_ +from sugar_network.client import Connection, keyfile, api, packagekit, injector as injector_, model from sugar_network.client.injector import _PreemptivePool, Injector +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, lsb_release @@ -398,7 +399,7 @@ class InjectorTest(tests.Test): activity_bundle = self.zips(('topdir/activity/activity.info', activity_info)) release = conn.upload(['context'], activity_bundle, cmd='submit', initial=True) - self.assertRaises(RuntimeError, injector._solve, 'context', 'stable') + self.assertRaises(http.ServiceUnavailable, injector._solve, 'context', 'stable') def test_solve_ReuseCachedSolution(self): volume = self.start_master() @@ -514,7 +515,7 @@ class InjectorTest(tests.Test): self.assertEqual([client.api.value, 'stable', 0], json.load(file('client/solutions/context'))[:-1]) os.unlink('client/solutions/context') - self.assertRaises(RuntimeError, injector._solve, 'context', 'stable') + self.assertRaises(http.ServiceUnavailable, injector._solve, 'context', 'stable') def test_download_SetExecPermissions(self): volume = self.start_master() @@ -575,6 +576,7 @@ class InjectorTest(tests.Test): ], [i for i in injector.checkin('context')]) + self.assertEqual(['checkin'], this.volume['context']['context']['pins']) self.assertEqual(activity_info, file(join('client', 'releases', release, 'activity', 'activity.info')).read()) self.assertEqual([client.api.value, 'stable', 0, { 'context': { @@ -626,6 +628,7 @@ class InjectorTest(tests.Test): }, json.load(file('client/checkins'))) self.assertEqual(0, injector._pool._du) + self.assertEqual(['checkin'], this.volume['context']['context']['pins']) assert injector.checkout('context') assert exists(join('client', 'releases', release)) @@ -633,6 +636,7 @@ class InjectorTest(tests.Test): }, json.load(file('client/checkins'))) self.assertEqual(len(activity_info), injector._pool._du) + self.assertEqual([], this.volume['context']['context']['pins']) for __ in injector.checkin('context'): pass @@ -642,6 +646,7 @@ class InjectorTest(tests.Test): }, json.load(file('client/checkins'))) self.assertEqual(0, injector._pool._du) + self.assertEqual(['checkin'], this.volume['context']['context']['pins']) assert injector.checkout('context') assert not injector.checkout('context') @@ -651,6 +656,7 @@ class InjectorTest(tests.Test): }, json.load(file('client/checkins'))) self.assertEqual(len(activity_info), injector._pool._du) + self.assertEqual([], this.volume['context']['context']['pins']) def test_checkin_Refresh(self): volume = self.start_master() diff --git a/tests/units/client/journal.py b/tests/units/client/journal.py index 30c67f8..bae636b 100755 --- a/tests/units/client/journal.py +++ b/tests/units/client/journal.py @@ -161,10 +161,14 @@ class JournalTest(tests.Test): request = Request() request.path = ['journal', 'guid1', 'preview'] response = Response() + blob = ds.journal_get_preview(request, response) self.assertEqual({ - 'mime_type': 'image/png', - 'blob': '.sugar/default/datastore/gu/guid1/metadata/preview', - }, ds.journal_get_preview(request, response)) + 'content-type': 'image/png', + }, + dict(blob)) + self.assertEqual( + '.sugar/default/datastore/gu/guid1/metadata/preview', + blob.path) self.assertEqual(None, response.content_type) diff --git a/tests/units/client/offline_routes.py b/tests/units/client/offline_routes.py deleted file mode 100755 index c8ac58c..0000000 --- a/tests/units/client/offline_routes.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/env python -# sugar-lint: disable - -import json -import hashlib -from cStringIO import StringIO -from zipfile import ZipFile -from os.path import exists - -from __init__ import tests, src_root - -from sugar_network import client, model -from sugar_network.client import IPCConnection, releases, packagekit -from sugar_network.client.routes import ClientRoutes -from sugar_network.model.user import User -from sugar_network.model.report import Report -from sugar_network.toolkit.router import Router -from sugar_network.toolkit import coroutine, http, lsb_release - - -class OfflineRoutes(tests.Test): - - def setUp(self, fork_num=0): - tests.Test.setUp(self, fork_num) - self.override(releases, '_activity_id_new', lambda: 'activity_id') - - def test_whoami(self): - ipc = self.start_offline_client() - - self.assertEqual( - {'guid': tests.UID, 'roles': [], 'route': 'offline'}, - ipc.get(cmd='whoami')) - - def test_Events(self): - ipc = self.start_offline_client() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - job = coroutine.spawn(read_events) - coroutine.dispatch() - - guid = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - ipc.put(['context', guid], { - 'title': 'title_2', - }) - ipc.delete(['context', guid]) - coroutine.sleep(.1) - job.kill() - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - - def test_Feeds(self): - ipc = self.start_offline_client() - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - impl1 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - self.home_volume['release'].update(impl1, {'data': { - 'spec': {'*-*': {}}, - }}) - impl2 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '2', - 'stability': 'stable', - 'notes': '', - }) - self.home_volume['release'].update(impl2, {'data': { - 'spec': {'*-*': { - 'requires': { - 'dep1': {}, - 'dep2': {'restrictions': [['1', '2']]}, - 'dep3': {'restrictions': [[None, '2']]}, - 'dep4': {'restrictions': [['3', None]]}, - }, - }}, - }}) - - self.assertEqual({ - 'releases': [ - { - 'version': '1', - 'stability': 'stable', - 'guid': impl1, - 'license': ['GPLv3+'], - 'layer': ['local'], - 'author': {}, - 'ctime': self.home_volume['release'].get(impl1).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': {'spec': {'*-*': {}}}, - }, - { - 'version': '2', - 'stability': 'stable', - 'guid': impl2, - 'license': ['GPLv3+'], - 'layer': ['local'], - 'author': {}, - 'ctime': self.home_volume['release'].get(impl2).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'spec': {'*-*': { - 'requires': { - 'dep1': {}, - 'dep2': {'restrictions': [['1', '2']]}, - 'dep3': {'restrictions': [[None, '2']]}, - 'dep4': {'restrictions': [['3', None]]}, - }, - }}, - }, - }, - ], - }, - ipc.get(['context', context], cmd='feed')) - - def test_BLOBs(self): - ipc = self.start_offline_client() - - guid = ipc.post(['context'], { - 'type': 'package', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - blob = 'logo_blob' - ipc.request('PUT', ['context', guid, 'logo'], blob) - - self.assertEqual( - blob, - ipc.request('GET', ['context', guid, 'logo']).content) - self.assertEqual({ - 'logo': { - 'url': 'http://127.0.0.1:5555/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['logo'])) - self.assertEqual([{ - 'logo': { - 'url': 'http://127.0.0.1:5555/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['logo'])['result']) - - self.assertEqual( - file(src_root + '/sugar_network/static/httpdocs/images/package.png').read(), - ipc.request('GET', ['context', guid, 'icon']).content) - self.assertEqual({ - 'icon': { - 'url': 'http://127.0.0.1:5555/static/images/package.png', - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['icon'])) - self.assertEqual([{ - 'icon': { - 'url': 'http://127.0.0.1:5555/static/images/package.png', - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['icon'])['result']) - - def test_favorite(self): - ipc = self.start_offline_client() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - context1 = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title1', - 'summary': 'summary', - 'description': 'description', - }) - context2 = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title2', - 'summary': 'summary', - 'description': 'description', - }) - - self.assertEqual( - sorted([]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='local')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['local']}, {'guid': context2, 'layer': ['local']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['local']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['local'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['local']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['local'], ipc.get(['context', context2, 'layer'])) - - del events[:] - ipc.put(['context', context1], True, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual( - {'guid': context1, 'resource': 'context', 'event': 'update'}, - events[-1]) - self.assertEqual( - sorted([{'guid': context1}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='local')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['favorite', 'local']}, {'guid': context2, 'layer': ['local']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['favorite', 'local']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['favorite', 'local'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['local']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['local'], ipc.get(['context', context2, 'layer'])) - - del events[:] - ipc.put(['context', context2], True, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual( - {'guid': context2, 'resource': 'context', 'event': 'update'}, - events[-1]) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='local')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['favorite', 'local']}, {'guid': context2, 'layer': ['favorite', 'local']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['favorite', 'local']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['favorite', 'local'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['favorite', 'local']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['favorite', 'local'], ipc.get(['context', context2, 'layer'])) - - del events[:] - ipc.put(['context', context1], False, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual( - {'guid': context1, 'resource': 'context', 'event': 'update'}, - events[-1]) - self.assertEqual( - sorted([{'guid': context2}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='local')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['local']}, {'guid': context2, 'layer': ['favorite', 'local']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['local']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['local'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['favorite', 'local']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['favorite', 'local'], ipc.get(['context', context2, 'layer'])) - - def test_launch_Activity(self): - local = self.start_online_client() - ipc = IPCConnection() - - activity_info = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license=Public Domain', - ]) - blob = self.zips(['TestActivity/activity/activity.info', activity_info]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit', initial=True) - - ipc.put(['context', 'bundle_id'], True, cmd='clone') - solution = [{ - 'guid': impl, - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'path': tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl), - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'unpack_size': len(activity_info), - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - }, - }] - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - self.node.stop() - coroutine.sleep(.1) - - log_path = tests.tmpdir + '/.sugar/default/logs/bundle_id.log' - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'exit', 'activity_id': 'activity_id'}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - def test_ServiceUnavailableWhileSolving(self): - ipc = self.start_offline_client() - - self.assertEqual([ - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'ServiceUnavailable', 'error': "Resource 'foo' does not exist in 'context'"}, - ], - [i for i in ipc.get(['context', 'foo'], cmd='launch')]) - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual([ - {'event': 'launch', 'activity_id': 'activity_id'}, - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'ServiceUnavailable', - 'stability': ['stable'], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - 'error': """\ -Can't find all required implementations: -- %s -> (problem) - No known implementations at all""" % context}, - ], - [i for i in ipc.get(['context', context], cmd='launch')]) - - impl = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'layer': ['origin'], - }) - self.home_volume['release'].update(impl, {'data': { - 'spec': { - '*-*': { - 'commands': {'activity': {'exec': 'true'}}, - 'requires': {'dep': {}}, - }, - }, - }}) - self.assertEqual([ - {'event': 'launch', 'activity_id': 'activity_id'}, - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'ServiceUnavailable', - 'stability': ['stable'], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - 'error': """\ -Can't find all required implementations: -- %s -> 1 (%s) -- dep -> (problem) - No known implementations at all""" % (context, impl)}, - ], - [i for i in ipc.get(['context', context], cmd='launch')]) - assert not exists('solutions/%s/%s' % (context[:2], context)) - - def test_ServiceUnavailableWhileInstalling(self): - ipc = self.start_offline_client() - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - impl = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'layer': ['origin'], - }) - self.home_volume['release'].update(impl, {'data': { - 'spec': { - '*-*': { - 'commands': {'activity': {'exec': 'true'}}, - 'requires': {'dep': {}}, - }, - }, - }}) - ipc.post(['context'], { - 'guid': 'dep', - 'type': 'package', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - 'aliases': { - lsb_release.distributor_id(): { - 'status': 'success', - 'binary': [['dep.bin']], - }, - }, - }) - - def resolve(names): - return dict([(i, {'name': i, 'pk_id': i, 'version': '0', 'arch': '*', 'installed': False}) for i in names]) - self.override(packagekit, 'resolve', resolve) - - self.assertEqual([ - {'event': 'launch', 'activity_id': 'activity_id'}, - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'ServiceUnavailable', 'error': 'Installation is not available in offline', - 'stability': ['stable'], - 'solution': [ - { 'guid': impl, - 'context': context, - 'license': ['GPLv3+'], - 'stability': 'stable', - 'version': '1', - 'layer': ['origin', 'local'], - 'author': {}, - 'ctime': self.home_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {'dep': {}}}}, - }, - }, - { 'guid': 'dep', - 'context': 'dep', - 'install': [{'arch': '*', 'installed': False, 'name': 'dep.bin', 'pk_id': 'dep.bin', 'version': '0'}], - 'license': None, - 'stability': 'packaged', - 'version': '0', - }, - ], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - }, - ], - [i for i in ipc.get(['context', context], cmd='launch')]) - - def test_NoAuthors(self): - ipc = self.start_offline_client() - - guid = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual( - {}, - self.home_volume['context'].get(guid)['author']) - self.assertEqual( - [], - ipc.get(['context', guid, 'author'])) - - def test_HandleDeletes(self): - ipc = self.start_offline_client() - - guid = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - - guid_path = 'db/context/%s/%s' % (guid[:2], guid) - assert exists(guid_path) - - ipc.delete(['context', guid]) - self.assertRaises(http.NotFound, ipc.get, ['context', guid]) - assert not exists(guid_path) - - def test_SubmitReport(self): - ipc = self.home_volume = self.start_offline_client() - - self.touch( - ['file1', 'content1'], - ['file2', 'content2'], - ['file3', 'content3'], - ) - events = [i for i in ipc.post(['report'], {'context': 'context', 'error': 'error', 'logs': [ - tests.tmpdir + '/file1', - tests.tmpdir + '/file2', - tests.tmpdir + '/file3', - ]}, cmd='submit')] - self.assertEqual('done', events[-1]['event']) - guid = events[-1]['guid'] - - self.assertEqual({ - 'context': 'context', - 'error': 'error', - }, - ipc.get(['report', guid], reply=['context', 'error'])) - zipfile = ZipFile('db/report/%s/%s/data.blob' % (guid[:2], guid)) - self.assertEqual('content1', zipfile.read('file1')) - self.assertEqual('content2', zipfile.read('file2')) - self.assertEqual('content3', zipfile.read('file3')) - - -if __name__ == '__main__': - tests.main() diff --git a/tests/units/client/online_routes.py b/tests/units/client/online_routes.py deleted file mode 100755 index 50df2ec..0000000 --- a/tests/units/client/online_routes.py +++ /dev/null @@ -1,1611 +0,0 @@ -#!/usr/bin/env python -# sugar-lint: disable - -import os -import json -import time -import copy -import shutil -import zipfile -import hashlib -from zipfile import ZipFile -from cStringIO import StringIO -from os.path import exists, lexists, basename - -from __init__ import tests, src_root - -from sugar_network import client, db, model -from sugar_network.client import IPCConnection, journal, routes, releases -from sugar_network.toolkit import coroutine, http -from sugar_network.toolkit.spec import Spec -from sugar_network.client.routes import ClientRoutes, Request, Response -from sugar_network.node.master import MasterRoutes -from sugar_network.db import Volume, Resource -from sugar_network.model.user import User -from sugar_network.model.report import Report -from sugar_network.model.context import Context -from sugar_network.model.release import Release -from sugar_network.model.post import Post -from sugar_network.toolkit.router import route -from sugar_network.toolkit import Option - -import requests - - -class OnlineRoutes(tests.Test): - - def setUp(self, fork_num=0): - tests.Test.setUp(self, fork_num) - self.override(releases, '_activity_id_new', lambda: 'activity_id') - - def test_whoami(self): - self.start_online_client() - ipc = IPCConnection() - - self.assertEqual( - {'guid': tests.UID, 'roles': [], 'route': 'proxy'}, - ipc.get(cmd='whoami')) - - def test_Events(self): - local_volume = self.start_online_client() - ipc = IPCConnection() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - guid = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - ipc.put(['context', guid], { - 'title': 'title_2', - }) - coroutine.sleep(.1) - ipc.delete(['context', guid]) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': tests.UID, 'resource': 'user', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - guid = self.node_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.node_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - self.node_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - guid = local_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - local_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - local_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([], events) - - self.node.stop() - coroutine.sleep(.1) - del events[:] - - guid = local_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - local_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - local_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - def test_Feeds(self): - self.start_online_client() - ipc = IPCConnection() - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - impl1 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['release'].update(impl1, {'data': { - 'spec': {'*-*': {}}, - }}) - impl2 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '2', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['release'].update(impl2, {'data': { - 'spec': {'*-*': { - 'requires': { - 'dep1': {}, - 'dep2': {'restrictions': [['1', '2']]}, - 'dep3': {'restrictions': [[None, '2']]}, - 'dep4': {'restrictions': [['3', None]]}, - }, - }}, - 'blob_size': 1, - 'unpack_size': 2, - 'mime_type': 'foo', - }}) - - self.assertEqual({ - 'releases': [ - { - 'version': '1', - 'stability': 'stable', - 'guid': impl1, - 'license': ['GPLv3+'], - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl1).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': {'spec': {'*-*': {}}}, - }, - { - 'version': '2', - 'stability': 'stable', - 'guid': impl2, - 'license': ['GPLv3+'], - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl2).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'spec': {'*-*': { - 'requires': { - 'dep1': {}, - 'dep2': {'restrictions': [['1', '2']]}, - 'dep3': {'restrictions': [[None, '2']]}, - 'dep4': {'restrictions': [['3', None]]}, - }, - }}, - 'blob_size': 1, - 'unpack_size': 2, - 'mime_type': 'foo', - }, - }, - ], - }, - ipc.get(['context', context], cmd='feed')) - - def test_BLOBs(self): - self.start_online_client() - ipc = IPCConnection() - - guid = ipc.post(['context'], { - 'type': 'package', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - blob = 'logo_blob' - ipc.request('PUT', ['context', guid, 'logo'], blob, headers={'content-type': 'image/png'}) - - self.assertEqual( - blob, - ipc.request('GET', ['context', guid, 'logo']).content) - self.assertEqual({ - 'logo': { - 'url': 'http://127.0.0.1:8888/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['logo'])) - self.assertEqual([{ - 'logo': { - 'url': 'http://127.0.0.1:8888/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['logo'])['result']) - - self.assertEqual( - file(src_root + '/sugar_network/static/httpdocs/images/package.png').read(), - ipc.request('GET', ['context', guid, 'icon']).content) - self.assertEqual({ - 'icon': { - 'url': 'http://127.0.0.1:8888/static/images/package.png', - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['icon'])) - self.assertEqual([{ - 'icon': { - 'url': 'http://127.0.0.1:8888/static/images/package.png', - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['icon'])['result']) - - def test_favorite(self): - local = self.start_online_client() - ipc = IPCConnection() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - context1 = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title1', - 'summary': 'summary', - 'description': 'description', - 'layer': ['foo'], - }) - context2 = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title2', - 'summary': 'summary', - 'description': 'description', - 'layer': ['foo'], - }) - - self.assertEqual( - sorted([]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='foo')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['foo']}, {'guid': context2, 'layer': ['foo']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['foo']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['foo'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['foo']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['foo'], ipc.get(['context', context2, 'layer'])) - self.assertEqual( - sorted([]), - sorted([i['layer'] for i in local['context'].find(reply='layer')[0]])) - - del events[:] - ipc.put(['context', context1], True, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': context1, 'resource': 'context', 'event': 'update'}, - ], - events) - self.assertEqual( - sorted([{'guid': context1}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='foo')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['foo', 'favorite']}, {'guid': context2, 'layer': ['foo']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['foo', 'favorite']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['foo', 'favorite'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['foo']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['foo'], ipc.get(['context', context2, 'layer'])) - self.assertEqual( - sorted([['foo', 'favorite']]), - sorted([i['layer'] for i in local['context'].find(reply='layer')[0]])) - - del events[:] - ipc.put(['context', context2], True, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': context2, 'resource': 'context', 'event': 'update'}, - ], - events) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='foo')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['foo', 'favorite']}, {'guid': context2, 'layer': ['foo', 'favorite']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['foo', 'favorite']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['foo', 'favorite'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['foo', 'favorite']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['foo', 'favorite'], ipc.get(['context', context2, 'layer'])) - self.assertEqual( - sorted([(context1, ['foo', 'favorite']), (context2, ['foo', 'favorite'])]), - sorted([(i.guid, i['layer']) for i in local['context'].find(reply='layer')[0]])) - - del events[:] - ipc.put(['context', context1], False, cmd='favorite') - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': context1, 'resource': 'context', 'event': 'update'}, - ], - events) - self.assertEqual( - sorted([{'guid': context2}]), - sorted(ipc.get(['context'], layer='favorite')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'], layer='foo')['result'])) - self.assertEqual( - sorted([{'guid': context1}, {'guid': context2}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context1, 'layer': ['foo']}, {'guid': context2, 'layer': ['foo', 'favorite']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['foo']}, ipc.get(['context', context1], reply='layer')) - self.assertEqual(['foo'], ipc.get(['context', context1, 'layer'])) - self.assertEqual({'layer': ['foo', 'favorite']}, ipc.get(['context', context2], reply='layer')) - self.assertEqual(['foo', 'favorite'], ipc.get(['context', context2, 'layer'])) - self.assertEqual( - sorted([(context1, ['foo']), (context2, ['foo', 'favorite'])]), - sorted([(i.guid, i['layer']) for i in local['context'].find(reply='layer')[0]])) - - def test_clone_Fails(self): - self.start_online_client([User, Context, Release]) - conn = IPCConnection() - - self.assertEqual([ - {'event': 'failure', 'exception': 'NotFound', 'error': "Resource 'foo' does not exist in 'context'"}, - ], - [i for i in conn.put(['context', 'foo'], True, cmd='clone')]) - - context = conn.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - - self.assertEqual([ - {'event': 'failure', 'exception': 'NotFound', - 'stability': ['stable'], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - 'error': """\ -Can't find all required implementations: -- %s -> (problem) - No known implementations at all""" % context}, - ], - [i for i in conn.put(['context', context], True, cmd='clone')]) - - assert not exists('solutions/%s/%s' % (context[:2], context)) - - impl = conn.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['release'].update(impl, {'data': { - 'blob_size': 1, - 'spec': { - '*-*': { - 'commands': { - 'activity': { - 'exec': 'echo', - }, - }, - }, - }, - }}) - - self.assertEqual([ - {'event': 'failure', 'exception': 'NotFound', 'error': 'BLOB does not exist', - 'stability': ['stable'], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - 'solution': [{ - 'guid': impl, - 'context': context, - 'license': ['GPLv3+'], - 'stability': 'stable', - 'version': '1', - 'path': tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl), - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'spec': { - '*-*': { - 'commands': { - 'activity': { - 'exec': 'echo', - }, - }, - }, - }, - 'blob_size': 1, - }, - }], - }, - ], - [i for i in conn.put(['context', context], True, cmd='clone')]) - assert not exists('solutions/%s/%s' % (context[:2], context)) - - def test_clone_Content(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - context = ipc.post(['context'], { - 'type': 'book', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - impl = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - blob = 'content' - self.node_volume['release'].update(impl, {'data': {'blob': StringIO(blob), 'foo': 'bar'}}) - clone_path = 'client/context/%s/%s/.clone' % (context[:2], context) - solution = [{ - 'guid': impl, - 'context': context, - 'license': ['GPLv3+'], - 'version': '1', - 'stability': 'stable', - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'foo': 'bar', - 'blob_size': len(blob), - }, - }] - - self.assertEqual([ - {'event': 'ready'}, - ], - [i for i in ipc.put(['context', context], True, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': context, - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': context}]), - sorted(ipc.get(['context'], layer='clone')['result'])) - self.assertEqual( - sorted([{'guid': context}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': context, 'layer': ['clone']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['clone']}, ipc.get(['context', context], reply='layer')) - self.assertEqual(['clone'], ipc.get(['context', context, 'layer'])) - self.assertEqual( - [(context, ['clone'])], - [(i.guid, i['layer']) for i in local['context'].find(reply='layer')[0]]) - self.assertEqual({ - 'layer': ['clone'], - 'type': ['book'], - 'author': {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, - 'title': {'en-us': 'title'}, - }, - local['context'].get(context).properties(['layer', 'type', 'author', 'title'])) - self.assertEqual({ - 'context': context, - 'license': ['GPLv3+'], - 'version': '1', - 'stability': 'stable', - }, - local['release'].get(impl).properties(['context', 'license', 'version', 'stability'])) - blob_path = 'client/release/%s/%s/data.blob' % (impl[:2], impl) - solution[0]['path'] = tests.tmpdir + '/' + blob_path - self.assertEqual({ - 'seqno': 5, - 'blob_size': len(blob), - 'blob': tests.tmpdir + '/' + blob_path, - 'mtime': int(os.stat(blob_path[:-5]).st_mtime), - 'foo': 'bar', - }, - local['release'].get(impl).meta('data')) - self.assertEqual('content', file(blob_path).read()) - assert exists(clone_path + '/data.blob') - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/%s/%s' % (context[:2], context)))) - - self.assertEqual([ - ], - [i for i in ipc.put(['context', context], False, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': context, - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': context, 'layer': []}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': []}, ipc.get(['context', context], reply='layer')) - self.assertEqual([], ipc.get(['context', context, 'layer'])) - self.assertEqual({ - 'layer': [], - 'type': ['book'], - 'author': {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, - 'title': {'en-us': 'title'}, - }, - local['context'].get(context).properties(['layer', 'type', 'author', 'title'])) - blob_path = 'client/release/%s/%s/data.blob' % (impl[:2], impl) - self.assertEqual({ - 'seqno': 5, - 'blob_size': len(blob), - 'blob': tests.tmpdir + '/' + blob_path, - 'mtime': int(os.stat(blob_path[:-5]).st_mtime), - 'foo': 'bar', - }, - local['release'].get(impl).meta('data')) - self.assertEqual('content', file(blob_path).read()) - assert not lexists(clone_path) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/%s/%s' % (context[:2], context)))) - - self.assertEqual([ - {'event': 'ready'}, - ], - [i for i in ipc.put(['context', context], True, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': context, - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': context, 'layer': ['clone']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - assert exists(clone_path + '/data.blob') - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/%s/%s' % (context[:2], context)))) - - def test_clone_Activity(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - activity_info = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license=Public Domain', - ]) - blob = self.zips(['TestActivity/activity/activity.info', activity_info]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit', initial=True) - clone_path = 'client/context/bu/bundle_id/.clone' - blob_path = tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl) - solution = [{ - 'guid': impl, - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'unpack_size': len(activity_info), - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - }, - }] - downloaded_solution = copy.deepcopy(solution) - downloaded_solution[0]['path'] = blob_path - - self.assertEqual([ - {'event': 'ready'}, - ], - [i for i in ipc.put(['context', 'bundle_id'], True, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': 'bundle_id', - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': 'bundle_id'}]), - sorted(ipc.get(['context'], layer='clone')['result'])) - self.assertEqual( - sorted([{'guid': 'bundle_id'}]), - sorted(ipc.get(['context'])['result'])) - self.assertEqual( - sorted([{'guid': 'bundle_id', 'layer': ['clone']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': ['clone']}, ipc.get(['context', 'bundle_id'], reply='layer')) - self.assertEqual(['clone'], ipc.get(['context', 'bundle_id', 'layer'])) - self.assertEqual({ - 'layer': ['clone'], - 'type': ['activity'], - 'author': {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, - 'title': {'en-us': 'TestActivity'}, - }, - local['context'].get('bundle_id').properties(['layer', 'type', 'author', 'title'])) - self.assertEqual({ - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'version': '1', - 'stability': 'stable', - }, - local['release'].get(impl).properties(['context', 'license', 'version', 'stability'])) - self.assertEqual({ - 'seqno': 5, - 'unpack_size': len(activity_info), - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'blob': blob_path, - 'mtime': int(os.stat(blob_path[:-5]).st_mtime), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': { - '*-*': { - 'requires': {}, - 'commands': {'activity': {'exec': 'true'}}, - }, - }, - }, - local['release'].get(impl).meta('data')) - self.assertEqual(activity_info, file(blob_path + '/activity/activity.info').read()) - assert exists(clone_path + '/data.blob/activity/activity.info') - self.assertEqual( - [client.api.value, ['stable'], downloaded_solution], - json.load(file('solutions/bu/bundle_id'))) - - self.assertEqual([ - ], - [i for i in ipc.put(['context', 'bundle_id'], False, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': 'bundle_id', - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': 'bundle_id', 'layer': []}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - self.assertEqual({'layer': []}, ipc.get(['context', 'bundle_id'], reply='layer')) - self.assertEqual([], ipc.get(['context', 'bundle_id', 'layer'])) - self.assertEqual({ - 'layer': [], - 'type': ['activity'], - 'author': {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, - 'title': {'en-us': 'TestActivity'}, - }, - local['context'].get('bundle_id').properties(['layer', 'type', 'author', 'title'])) - self.assertEqual({ - 'seqno': 5, - 'unpack_size': len(activity_info), - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'blob': blob_path, - 'mtime': int(os.stat(blob_path[:-5]).st_mtime), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': { - '*-*': { - 'requires': {}, - 'commands': {'activity': {'exec': 'true'}}, - }, - }, - }, - local['release'].get(impl).meta('data')) - self.assertEqual(activity_info, file(blob_path + '/activity/activity.info').read()) - assert not exists(clone_path) - self.assertEqual( - [client.api.value, ['stable'], downloaded_solution], - json.load(file('solutions/bu/bundle_id'))) - - self.assertEqual([ - {'event': 'ready'}, - ], - [i for i in ipc.put(['context', 'bundle_id'], True, cmd='clone')]) - - self.assertEqual({ - 'event': 'update', - 'guid': 'bundle_id', - 'resource': 'context', - }, - events[-1]) - self.assertEqual( - sorted([{'guid': 'bundle_id', 'layer': ['clone']}]), - sorted(ipc.get(['context'], reply='layer')['result'])) - assert exists(clone_path + '/data.blob/activity/activity.info') - self.assertEqual( - [client.api.value, ['stable'], downloaded_solution], - json.load(file('solutions/bu/bundle_id'))) - - def test_clone_ActivityWithStabilityPreferences(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - - activity_info1 = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - ]) - blob1 = self.zips(['TestActivity/activity/activity.info', activity_info1]) - impl1 = ipc.upload(['release'], StringIO(blob1), cmd='submit', initial=True) - - activity_info2 = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - 'stability = buggy', - ]) - blob2 = self.zips(['TestActivity/activity/activity.info', activity_info2]) - impl2 = ipc.upload(['release'], StringIO(blob2), cmd='submit', initial=True) - - self.assertEqual( - 'ready', - [i for i in ipc.put(['context', 'bundle_id'], True, cmd='clone')][-1]['event']) - - coroutine.dispatch() - self.assertEqual({'layer': ['clone']}, ipc.get(['context', 'bundle_id'], reply='layer')) - self.assertEqual([impl1], [i.guid for i in local['release'].find()[0]]) - self.assertEqual(impl1, basename(os.readlink('client/context/bu/bundle_id/.clone'))) - - self.touch(('config', [ - '[stabilities]', - 'bundle_id = buggy stable', - ])) - Option.load(['config']) - - self.assertEqual( - [], - [i for i in ipc.put(['context', 'bundle_id'], False, cmd='clone')]) - self.assertEqual( - 'ready', - [i for i in ipc.put(['context', 'bundle_id'], True, cmd='clone')][-1]['event']) - - coroutine.dispatch() - self.assertEqual({'layer': ['clone']}, ipc.get(['context', 'bundle_id'], reply='layer')) - self.assertEqual([impl1, impl2], [i.guid for i in local['release'].find()[0]]) - self.assertEqual(impl2, basename(os.readlink('client/context/bu/bundle_id/.clone'))) - - def test_clone_Head(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - - activity_info = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - ]) - blob = self.zips(['TestActivity/activity/activity.info', activity_info]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit', initial=True) - blob_path = 'master/release/%s/%s/data.blob' % (impl[:2], impl) - - self.assertEqual({ - 'guid': impl, - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'context': 'bundle_id', - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - 'unpack_size': len(activity_info), - }, - }, - ipc.head(['context', 'bundle_id'], cmd='clone')) - - self.assertEqual( - 'ready', - [i for i in ipc.put(['context', 'bundle_id'], True, cmd='clone')][-1]['event']) - blob_path = tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl) - - self.assertEqual({ - 'guid': impl, - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'context': 'bundle_id', - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'blob': blob_path, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'application/vnd.olpc-sugar', - 'mtime': int(os.stat(blob_path[:-5]).st_mtime), - 'seqno': 5, - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - 'unpack_size': len(activity_info), - }, - }, - ipc.head(['context', 'bundle_id'], cmd='clone')) - - def test_launch_Activity(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - - activity_info = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license=Public Domain', - ]) - blob = self.zips(['TestActivity/activity/activity.info', activity_info]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit', initial=True) - coroutine.sleep(.1) - - solution = [{ - 'guid': impl, - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'unpack_size': len(activity_info), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - }, - }] - downloaded_solution = copy.deepcopy(solution) - downloaded_solution[0]['path'] = tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl) - log_path = tests.tmpdir + '/.sugar/default/logs/bundle_id.log' - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'exit', 'activity_id': 'activity_id'}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], downloaded_solution], - json.load(file('solutions/bu/bundle_id'))) - - blob = self.zips(['TestActivity/activity/activity.info', [ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license=Public Domain', - ]]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit') - coroutine.sleep(.1) - - shutil.rmtree('solutions') - solution = [{ - 'guid': impl, - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '2', - 'path': tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl), - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'unpack_size': len(activity_info), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'true'}}, 'requires': {}}}, - }, - }] - log_path = tests.tmpdir + '/.sugar/default/logs/bundle_id_1.log' - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'exit', 'activity_id': 'activity_id'}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - self.node.stop() - coroutine.sleep(.1) - - log_path = tests.tmpdir + '/.sugar/default/logs/bundle_id_2.log' - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'exit', 'activity_id': 'activity_id'}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - shutil.rmtree('solutions') - log_path = tests.tmpdir + '/.sugar/default/logs/bundle_id_3.log' - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'exit', 'activity_id': 'activity_id'}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - def test_launch_Fails(self): - local = self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - - self.assertEqual([ - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'NotFound', 'error': "Resource 'foo' does not exist in 'context'"}, - ], - [i for i in ipc.get(['context', 'foo'], cmd='launch')]) - - ipc.post(['context'], { - 'guid': 'bundle_id', - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual([ - {'event': 'launch', 'activity_id': 'activity_id', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'NotFound', - 'stability': ['stable'], - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - ], - 'error': """\ -Can't find all required implementations: -- bundle_id -> (problem) - No known implementations at all"""}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - - activity_info = '\n'.join([ - '[Activity]', - 'name = TestActivity', - 'bundle_id = bundle_id', - 'exec = false', - 'icon = icon', - 'activity_version = 1', - 'license=Public Domain', - ]) - blob = self.zips(['TestActivity/activity/activity.info', activity_info]) - impl = ipc.upload(['release'], StringIO(blob), cmd='submit', initial=True) - - solution = [{ - 'guid': impl, - 'context': 'bundle_id', - 'license': ['Public Domain'], - 'stability': 'stable', - 'version': '1', - 'path': tests.tmpdir + '/client/release/%s/%s/data.blob' % (impl[:2], impl), - 'layer': ['origin'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': { - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'unpack_size': len(activity_info), - 'mime_type': 'application/vnd.olpc-sugar', - 'spec': {'*-*': {'commands': {'activity': {'exec': 'false'}}, 'requires': {}}}, - }, - }] - self.assertEqual([ - {'event': 'launch', 'foo': 'bar', 'activity_id': 'activity_id'}, - {'event': 'exec', 'activity_id': 'activity_id'}, - {'event': 'failure', 'activity_id': 'activity_id', 'exception': 'RuntimeError', 'error': 'Process exited with 1 status', - 'stability': ['stable'], - 'args': ['false', '-b', 'bundle_id', '-a', 'activity_id'], - 'solution': solution, - 'logs': [ - tests.tmpdir + '/.sugar/default/logs/shell.log', - tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', - tests.tmpdir + '/.sugar/default/logs/bundle_id.log', - ]}, - ], - [i for i in ipc.get(['context', 'bundle_id'], cmd='launch', foo='bar')]) - assert local['release'].exists(impl) - self.assertEqual( - [client.api.value, ['stable'], solution], - json.load(file('solutions/bu/bundle_id'))) - - def test_InvalidateSolutions(self): - self.start_online_client() - ipc = IPCConnection() - self.assertNotEqual(None, self.client_routes._node_mtime) - - mtime = self.client_routes._node_mtime - coroutine.sleep(1.1) - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - assert self.client_routes._node_mtime == mtime - - coroutine.sleep(1.1) - - impl1 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['release'].update(impl1, {'data': { - 'spec': {'*-*': {}}, - }}) - assert self.client_routes._node_mtime > mtime - - mtime = self.client_routes._node_mtime - coroutine.sleep(1.1) - - impl2 = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '2', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['release'].update(impl2, {'data': { - 'spec': {'*-*': { - 'requires': { - 'dep1': {}, - 'dep2': {'restrictions': [['1', '2']]}, - 'dep3': {'restrictions': [[None, '2']]}, - 'dep4': {'restrictions': [['3', None]]}, - }, - }}, - }}) - assert self.client_routes._node_mtime > mtime - - def test_NoNeedlessRemoteRequests(self): - home_volume = self.start_online_client() - ipc = IPCConnection() - - guid = ipc.post(['context'], { - 'type': 'book', - 'title': 'remote', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual( - {'title': 'remote'}, - ipc.get(['context', guid], reply=['title'])) - - home_volume['context'].create({ - 'guid': guid, - 'type': 'activity', - 'title': 'local', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual( - {'title': 'local'}, - ipc.get(['context', guid], reply=['title'])) - - def test_RestrictLayers(self): - self.start_online_client([User, Context, Release]) - ipc = IPCConnection() - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - 'layer': 'public', - }) - impl = ipc.post(['release'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - 'layer': 'public', - }) - self.node_volume['release'].update(impl, {'data': { - 'spec': {'*-*': {}}, - }}) - - self.assertEqual( - [{'guid': context, 'layer': ['public']}], - ipc.get(['context'], reply=['guid', 'layer'])['result']) - self.assertEqual( - [], - ipc.get(['context'], reply=['guid', 'layer'], layer='foo')['result']) - self.assertEqual( - [{'guid': context, 'layer': ['public']}], - ipc.get(['context'], reply=['guid', 'layer'], layer='public')['result']) - - self.assertEqual( - [{'guid': impl, 'layer': ['origin', 'public']}], - ipc.get(['release'], reply=['guid', 'layer'])['result']) - self.assertEqual( - [], - ipc.get(['release'], reply=['guid', 'layer'], layer='foo')['result']) - self.assertEqual( - [{'guid': impl, 'layer': ['origin', 'public']}], - ipc.get(['release'], reply=['guid', 'layer'], layer='public')['result']) - - self.assertEqual({ - 'releases': [{ - 'stability': 'stable', - 'guid': impl, - 'version': '1', - 'license': ['GPLv3+'], - 'layer': ['origin', 'public'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': {'spec': {'*-*': {}}}, - }], - }, - ipc.get(['context', context], cmd='feed')) - self.assertEqual({ - 'releases': [], - }, - ipc.get(['context', context], cmd='feed', layer='foo')) - self.assertEqual({ - 'releases': [{ - 'stability': 'stable', - 'guid': impl, - 'version': '1', - 'license': ['GPLv3+'], - 'layer': ['origin', 'public'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': {'spec': {'*-*': {}}}, - }], - }, - ipc.get(['context', context], cmd='feed', layer='public')) - - client.layers.value = ['foo', 'bar'] - - self.assertEqual( - [], - ipc.get(['context'], reply=['guid', 'layer'])['result']) - self.assertEqual( - [], - ipc.get(['context'], reply=['guid', 'layer'], layer='foo')['result']) - self.assertEqual( - [{'guid': context, 'layer': ['public']}], - ipc.get(['context'], reply=['guid', 'layer'], layer='public')['result']) - - self.assertEqual( - [], - ipc.get(['release'], reply=['guid', 'layer'])['result']) - self.assertEqual( - [], - ipc.get(['release'], reply=['guid', 'layer'], layer='foo')['result']) - self.assertEqual( - [{'guid': impl, 'layer': ['origin', 'public']}], - ipc.get(['release'], reply=['guid', 'layer'], layer='public')['result']) - - self.assertEqual({ - 'releases': [], - }, - ipc.get(['context', context], cmd='feed')) - self.assertEqual({ - 'releases': [], - }, - ipc.get(['context', context], cmd='feed', layer='foo')) - self.assertEqual({ - 'releases': [{ - 'stability': 'stable', - 'guid': impl, - 'version': '1', - 'license': ['GPLv3+'], - 'layer': ['origin', 'public'], - 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, - 'ctime': self.node_volume['release'].get(impl).ctime, - 'notes': {'en-us': ''}, - 'tags': [], - 'data': {'spec': {'*-*': {}}}, - }], - }, - ipc.get(['context', context], cmd='feed', layer='public')) - - def test_Redirects(self): - - class Document(Resource): - - @db.blob_property() - def blob1(self, value): - raise http.Redirect(prefix + 'blob2') - - @db.blob_property() - def blob3(self, value): - raise http.Redirect(client.api.value + prefix + 'blob4') - - self.start_online_client([User, Document]) - ipc = IPCConnection() - guid = ipc.post(['document'], {}) - prefix = '/document/' + guid + '/' - - response = requests.request('GET', client.api.value + prefix + 'blob1', allow_redirects=False) - self.assertEqual(303, response.status_code) - self.assertEqual(prefix + 'blob2', response.headers['Location']) - - response = requests.request('GET', client.api.value + prefix + 'blob3', allow_redirects=False) - self.assertEqual(303, response.status_code) - self.assertEqual(client.api.value + prefix + 'blob4', response.headers['Location']) - - def test_DoNotSwitchToOfflineOnRedirectFails(self): - - class Document(Resource): - - @db.blob_property() - def blob1(self, value): - raise http.Redirect(prefix + '/blob2') - - @db.blob_property() - def blob2(self, value): - raise http._ConnectionError() - - local_volume = self.start_online_client([User, Document]) - ipc = IPCConnection() - guid = ipc.post(['document'], {}) - prefix = client.api.value + '/document/' + guid + '/' - local_volume['document'].create({'guid': guid}) - - trigger = self.wait_for_events(ipc, event='inline', state='connecting') - try: - ipc.get(['document', guid, 'blob1']) - except Exception: - pass - assert trigger.wait(.1) is None - - trigger = self.wait_for_events(ipc, event='inline', state='connecting') - try: - ipc.get(['document', guid, 'blob2']) - except Exception: - pass - assert trigger.wait(.1) is not None - - def test_ContentDisposition(self): - self.start_online_client([User, Context, Release, Post]) - ipc = IPCConnection() - - post = ipc.post(['post'], { - 'type': 'object', - 'context': 'context', - 'title': 'title', - 'message': '', - }) - ipc.request('PUT', ['post', post, 'data'], 'blob', headers={'Content-Type': 'image/png'}) - - response = ipc.request('GET', ['post', post, 'data']) - self.assertEqual( - 'attachment; filename="Title.png"', - response.headers.get('Content-Disposition')) - - def test_FallbackToLocalSNOnRemoteTransportFails(self): - - class LocalRoutes(routes._LocalRoutes): - - @route('GET', cmd='sleep') - def sleep(self): - return 'local' - - @route('GET', cmd='yield_raw_and_sleep', - mime_type='application/octet-stream') - def yield_raw_and_sleep(self): - yield 'local' - - @route('GET', cmd='yield_json_and_sleep', - mime_type='application/json') - def yield_json_and_sleep(self): - yield '"local"' - - self.override(routes, '_LocalRoutes', LocalRoutes) - home_volume = self.start_client() - ipc = IPCConnection() - - self.assertEqual('local', ipc.get(cmd='sleep')) - self.assertEqual('local', ipc.get(cmd='yield_raw_and_sleep')) - self.assertEqual('local', ipc.get(cmd='yield_json_and_sleep')) - - class NodeRoutes(MasterRoutes): - - @route('GET', cmd='sleep') - def sleep(self): - coroutine.sleep(.5) - return 'remote' - - @route('GET', cmd='yield_raw_and_sleep', - mime_type='application/octet-stream') - def yield_raw_and_sleep(self): - for __ in range(33): - yield "remote\n" - coroutine.sleep(.5) - for __ in range(33): - yield "remote\n" - - @route('GET', cmd='yield_json_and_sleep', - mime_type='application/json') - def yield_json_and_sleep(self): - yield '"' - yield 'r' - coroutine.sleep(1) - yield 'emote"' - - node_pid = self.fork_master([User], NodeRoutes) - self.client_routes._remote_connect() - self.wait_for_events(ipc, event='inline', state='online').wait() - - ts = time.time() - self.assertEqual('remote', ipc.get(cmd='sleep')) - self.assertEqual('remote\n' * 66, ipc.get(cmd='yield_raw_and_sleep')) - self.assertEqual('remote', ipc.get(cmd='yield_json_and_sleep')) - assert time.time() - ts >= 2 - - def kill(): - coroutine.sleep(.5) - self.waitpid(node_pid) - - coroutine.spawn(kill) - self.assertEqual('local', ipc.get(cmd='sleep')) - assert not ipc.get(cmd='inline') - - node_pid = self.fork_master([User], NodeRoutes) - self.client_routes._remote_connect() - self.wait_for_events(ipc, event='inline', state='online').wait() - - coroutine.spawn(kill) - self.assertEqual('local', ipc.get(cmd='yield_raw_and_sleep')) - assert not ipc.get(cmd='inline') - - node_pid = self.fork_master([User], NodeRoutes) - self.client_routes._remote_connect() - self.wait_for_events(ipc, event='inline', state='online').wait() - - coroutine.spawn(kill) - self.assertEqual('local', ipc.get(cmd='yield_json_and_sleep')) - assert not ipc.get(cmd='inline') - - def test_ReconnectOnServerFall(self): - routes._RECONNECT_TIMEOUT = 1 - - node_pid = self.fork_master() - self.start_client() - ipc = IPCConnection() - self.wait_for_events(ipc, event='inline', state='online').wait() - - def shutdown(): - coroutine.sleep(.1) - self.waitpid(node_pid) - coroutine.spawn(shutdown) - self.wait_for_events(ipc, event='inline', state='offline').wait() - - self.fork_master() - self.wait_for_events(ipc, event='inline', state='online').wait() - - def test_SilentReconnectOnGatewayErrors(self): - - class Routes(object): - - subscribe_tries = 0 - - def __init__(self, *args): - pass - - @route('GET', cmd='status', mime_type='application/json') - def info(self): - return {'resources': {}} - - @route('GET', cmd='subscribe', mime_type='text/event-stream') - def subscribe(self, request=None, response=None, ping=False, **condition): - Routes.subscribe_tries += 1 - coroutine.sleep(.1) - if Routes.subscribe_tries % 2: - raise http.BadGateway() - else: - raise http.GatewayTimeout() - - node_pid = self.start_master(None, Routes) - self.start_client() - ipc = IPCConnection() - self.wait_for_events(ipc, event='inline', state='online').wait() - - def read_events(): - for event in ipc.subscribe(): - events.append(event) - events = [] - coroutine.spawn(read_events) - - coroutine.sleep(1) - self.assertEqual([], events) - assert Routes.subscribe_tries > 2 - - def test_inline(self): - routes._RECONNECT_TIMEOUT = 2 - - cp = ClientRoutes(Volume('client', model.RESOURCES), client.api.value) - assert not cp.inline() - - trigger = self.wait_for_events(cp, event='inline', state='online') - coroutine.sleep(.5) - self.start_master() - trigger.wait(.5) - assert trigger.value is None - assert not cp.inline() - - trigger.wait() - assert cp.inline() - - trigger = self.wait_for_events(cp, event='inline', state='offline') - self.node.stop() - trigger.wait() - assert not cp.inline() - - def test_SubmitReport(self): - self.home_volume = self.start_online_client([User, Context, Release, Report]) - ipc = IPCConnection() - - self.touch( - ['file1', 'content1'], - ['file2', 'content2'], - ['file3', 'content3'], - ) - events = [i for i in ipc.post(['report'], {'context': 'context', 'error': 'error', 'logs': [ - tests.tmpdir + '/file1', - tests.tmpdir + '/file2', - tests.tmpdir + '/file3', - ]}, cmd='submit')] - self.assertEqual('done', events[-1]['event']) - guid = events[-1]['guid'] - - self.assertEqual({ - 'context': 'context', - 'error': 'error', - }, - ipc.get(['report', guid], reply=['context', 'error'])) - zipfile = ZipFile('master/report/%s/%s/data.blob' % (guid[:2], guid)) - self.assertEqual('content1', zipfile.read('file1')) - self.assertEqual('content2', zipfile.read('file2')) - self.assertEqual('content3', zipfile.read('file3')) - - -if __name__ == '__main__': - tests.main() diff --git a/tests/units/client/routes.py b/tests/units/client/routes.py index 7cd03e6..325ac99 100755 --- a/tests/units/client/routes.py +++ b/tests/units/client/routes.py @@ -5,18 +5,22 @@ import os import json import time +import hashlib from cStringIO import StringIO from os.path import exists from __init__ import tests -from sugar_network import db, client, model, toolkit -from sugar_network.client import journal, IPCConnection, cache_limit, cache_lifetime +from sugar_network import db, client, toolkit +from sugar_network.client import journal, IPCConnection, cache_limit, cache_lifetime, api, injector, routes +from sugar_network.client.model import RESOURCES +from sugar_network.client.injector import Injector from sugar_network.client.routes import ClientRoutes, CachedClientRoutes -from sugar_network.model.user import User -from sugar_network.model.report import Report -from sugar_network.toolkit.router import Router, Request, Response -from sugar_network.toolkit import coroutine, i18n +from sugar_network.node.model import User +from sugar_network.node.master import MasterRoutes +from sugar_network.toolkit.router import Router, Request, Response, route +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit import coroutine, i18n, parcel, http import requests @@ -24,7 +28,7 @@ import requests class RoutesTest(tests.Test): def test_Hub(self): - volume = db.Volume('db', model.RESOURCES) + volume = db.Volume('db', RESOURCES) cp = ClientRoutes(volume) server = coroutine.WSGIServer( ('127.0.0.1', client.ipc_port.value), Router(cp)) @@ -47,8 +51,305 @@ class RoutesTest(tests.Test): response = requests.request('GET', url + '/hub/', allow_redirects=False) self.assertEqual(index_html, response.content) - def test_LocalLayers(self): - self.home_volume = self.start_online_client() + def test_I18nQuery(self): + os.environ['LANGUAGE'] = 'foo' + self.start_online_client() + ipc = IPCConnection() + + ipc.request('POST', [], ''.join(parcel.encode([ + ('push', None, [ + {'resource': 'context'}, + {'guid': '1', 'patch': { + 'guid': {'value': '1', 'mtime': 1}, + 'ctime': {'value': 1, 'mtime': 1}, + 'mtime': {'value': 1, 'mtime': 1}, + 'type': {'value': ['activity'], 'mtime': 1}, + 'summary': {'value': {}, 'mtime': 1}, + 'description': {'value': {}, 'mtime': 1}, + 'title': {'value': {'en-US': 'qwe', 'ru-RU': 'йцу'}, 'mtime': 1}, + }}, + {'guid': '2', 'patch': { + 'guid': {'value': '2', 'mtime': 1}, + 'ctime': {'value': 1, 'mtime': 1}, + 'mtime': {'value': 1, 'mtime': 1}, + 'type': {'value': ['activity'], 'mtime': 1}, + 'summary': {'value': {}, 'mtime': 1}, + 'description': {'value': {}, 'mtime': 1}, + 'title': {'value': {'en-US': 'qwerty', 'ru-RU': 'йцукен'}, 'mtime': 1}, + }}, + ]), + ], header={'to': '127.0.0.1:7777', 'from': 'slave'})), params={'cmd': 'push'}) + + self.assertEqual([ + {'guid': '1'}, + {'guid': '2'}, + ], + ipc.get(['context'], query='йцу')['result']) + self.assertEqual([ + {'guid': '1'}, + {'guid': '2'}, + ], + ipc.get(['context'], query='qwe')['result']) + + self.assertEqual([ + {'guid': '2'}, + ], + ipc.get(['context'], query='йцукен')['result']) + self.assertEqual([ + {'guid': '2'}, + ], + ipc.get(['context'], query='qwerty')['result']) + + def test_LanguagesFallbackInRequests(self): + self.start_online_client() + ipc = IPCConnection() + + ipc.request('POST', [], ''.join(parcel.encode([ + ('push', None, [ + {'resource': 'context'}, + {'guid': '1', 'patch': { + 'guid': {'value': '1', 'mtime': 1}, + 'ctime': {'value': 1, 'mtime': 1}, + 'mtime': {'value': 1, 'mtime': 1}, + 'type': {'value': ['activity'], 'mtime': 1}, + 'summary': {'value': {}, 'mtime': 1}, + 'description': {'value': {}, 'mtime': 1}, + 'title': {'value': {'en': '1', 'ru': '2', 'es': '3'}, 'mtime': 1}, + }}, + {'guid': '2', 'patch': { + 'guid': {'value': '2', 'mtime': 1}, + 'ctime': {'value': 1, 'mtime': 1}, + 'mtime': {'value': 1, 'mtime': 1}, + 'type': {'value': ['activity'], 'mtime': 1}, + 'summary': {'value': {}, 'mtime': 1}, + 'description': {'value': {}, 'mtime': 1}, + 'title': {'value': {'en': '1', 'ru': '2'}, 'mtime': 1}, + }}, + {'guid': '3', 'patch': { + 'guid': {'value': '3', 'mtime': 1}, + 'ctime': {'value': 1, 'mtime': 1}, + 'mtime': {'value': 1, 'mtime': 1}, + 'type': {'value': ['activity'], 'mtime': 1}, + 'summary': {'value': {}, 'mtime': 1}, + 'description': {'value': {}, 'mtime': 1}, + 'title': {'value': {'en': '1'}, 'mtime': 1}, + }}, + ]), + ], header={'to': '127.0.0.1:7777', 'from': 'slave'})), params={'cmd': 'push'}) + + i18n._default_langs = None + os.environ['LANGUAGE'] = 'es:ru:en' + ipc = IPCConnection() + self.assertEqual('3', ipc.get(['context', '1', 'title'])) + self.assertEqual('2', ipc.get(['context', '2', 'title'])) + self.assertEqual('1', ipc.get(['context', '3', 'title'])) + + i18n._default_langs = None + os.environ['LANGUAGE'] = 'ru:en' + ipc = IPCConnection() + self.assertEqual('2', ipc.get(['context', '1', 'title'])) + self.assertEqual('2', ipc.get(['context', '2', 'title'])) + self.assertEqual('1', ipc.get(['context', '3', 'title'])) + + i18n._default_langs = None + os.environ['LANGUAGE'] = 'en' + ipc = IPCConnection() + self.assertEqual('1', ipc.get(['context', '1', 'title'])) + self.assertEqual('1', ipc.get(['context', '2', 'title'])) + self.assertEqual('1', ipc.get(['context', '3', 'title'])) + + i18n._default_langs = None + os.environ['LANGUAGE'] = 'foo' + ipc = IPCConnection() + self.assertEqual('1', ipc.get(['context', '1', 'title'])) + self.assertEqual('1', ipc.get(['context', '2', 'title'])) + self.assertEqual('1', ipc.get(['context', '3', 'title'])) + + def test_whoami(self): + self.start_offline_client() + ipc = IPCConnection() + + self.assertEqual( + {'guid': tests.UID, 'roles': [], 'route': 'offline'}, + ipc.get(cmd='whoami')) + + self.fork_master() + self.wait_for_events(event='inline', state='online').wait() + + self.assertEqual( + {'guid': tests.UID, 'roles': [], 'route': 'proxy'}, + ipc.get(cmd='whoami')) + + def test_Events(self): + self.override(time, 'time', lambda: 0) + self.start_offline_client() + ipc = IPCConnection() + events = [] + + def read_events(): + for event in ipc.subscribe(): + if event['event'] not in ('commit', 'pong'): + events.append(event) + coroutine.spawn(read_events) + coroutine.dispatch() + + guid = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + ipc.put(['context', guid], { + 'title': 'title_2', + }) + ipc.delete(['context', guid]) + coroutine.sleep(.1) + + self.assertEqual([ + {'event': 'create', 'guid': guid, 'resource': 'context'}, + {'event': 'update', 'guid': guid, 'resource': 'context', 'props': {'mtime': 0, 'title': {'en-us': 'title_2'}}}, + {'event': 'delete', 'guid': guid, 'resource': 'context'}, + ], + events) + del events[:] + + self.fork_master() + self.wait_for_events(event='inline', state='online').wait() + coroutine.sleep(.1) + + self.assertEqual([ + {'event': 'inline', 'state': 'connecting'}, + {'event': 'inline', 'state': 'online'}, + ], + events) + del events[:] + + guid = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + ipc.put(['context', guid], { + 'title': 'title_2', + }) + coroutine.sleep(.1) + ipc.delete(['context', guid]) + coroutine.sleep(.1) + + self.assertEqual([ + {'event': 'create', 'guid': tests.UID, 'resource': 'user'}, + {'event': 'create', 'guid': guid, 'resource': 'context'}, + {'event': 'update', 'guid': guid, 'resource': 'context', 'props': {'mtime': 0, 'title': {'en-us': 'title_2'}}}, + {'event': 'delete', 'guid': guid, 'resource': 'context'}, + ], + events) + del events[:] + + def test_HomeVolumeEventsOnlyInOffline(self): + home_volume = self.start_offline_client() + ipc = IPCConnection() + events = [] + + def read_events(): + for event in ipc.subscribe(): + if event['event'] not in ('commit', 'pong'): + events.append(event) + coroutine.spawn(read_events) + coroutine.sleep(.1) + + guid = home_volume['context'].create({ + 'type': ['activity'], + 'title': {}, + 'summary': {}, + 'description': {}, + }) + home_volume['context'].update(guid, { + 'title': {'en': 'title_2'}, + }) + home_volume['context'].delete(guid) + coroutine.sleep(.1) + + self.assertEqual([ + {'guid': guid, 'resource': 'context', 'event': 'create'}, + {'guid': guid, 'resource': 'context', 'event': 'update', 'props': {'title': {'en': 'title_2'}}}, + {'guid': guid, 'event': 'delete', 'resource': 'context'}, + ], + events) + del events[:] + + self.fork_master() + self.wait_for_events(event='inline', state='online').wait() + coroutine.sleep(.1) + del events[:] + + guid = home_volume['context'].create({ + 'type': ['activity'], + 'title': {}, + 'summary': {}, + 'description': {}, + }) + home_volume['context'].update(guid, { + 'title': {'en': 'title_2'}, + }) + coroutine.sleep(.1) + home_volume['context'].delete(guid) + coroutine.sleep(.1) + + self.assertEqual([], events) + + def test_BLOBs(self): + self.start_offline_client() + ipc = IPCConnection() + + blob = 'blob_value' + digest = hashlib.sha1(blob).hexdigest() + + guid = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + ipc.request('PUT', ['context', guid, 'logo'], blob, headers={'content-type': 'image/png'}) + + self.assertEqual( + blob, + ipc.request('GET', ['context', guid, 'logo']).content) + self.assertEqual({ + 'logo': 'http://127.0.0.1:5555/blobs/%s' % digest, + }, + ipc.get(['context', guid], reply=['logo'])) + self.assertEqual([{ + 'logo': 'http://127.0.0.1:5555/blobs/%s' % digest, + }], + ipc.get(['context'], reply=['logo'])['result']) + + self.fork_master() + self.wait_for_events(event='inline', state='online').wait() + + guid = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + ipc.request('PUT', ['context', guid, 'logo'], blob, headers={'content-type': 'image/png'}) + + self.assertEqual( + blob, + ipc.request('GET', ['context', guid, 'logo']).content) + self.assertEqual({ + 'logo': 'http://127.0.0.1:7777/blobs/%s' % digest, + }, + ipc.get(['context', guid], reply=['logo'])) + self.assertEqual([{ + 'logo': 'http://127.0.0.1:7777/blobs/%s' % digest, + }], + ipc.get(['context'], reply=['logo'])['result']) + + def test_OnlinePins(self): + home_volume = self.start_online_client() ipc = IPCConnection() guid1 = ipc.post(['context'], { @@ -58,7 +359,7 @@ class RoutesTest(tests.Test): 'summary': 'summary', 'description': 'description', }) - ipc.upload(['release'], StringIO(self.zips(['TestActivity/activity/activity.info', [ + ipc.upload(['context'], self.zips(['TestActivity/activity/activity.info', [ '[Activity]', 'name = 2', 'bundle_id = context2', @@ -67,9 +368,9 @@ class RoutesTest(tests.Test): 'activity_version = 1', 'license = Public Domain', 'stability = stable', - ]])), cmd='submit', initial=True) + ]]), cmd='submit', initial=True) guid2 = 'context2' - ipc.upload(['release'], StringIO(self.zips(['TestActivity/activity/activity.info', [ + ipc.upload(['context'], self.zips(['TestActivity/activity/activity.info', [ '[Activity]', 'name = 3', 'bundle_id = context3', @@ -78,7 +379,7 @@ class RoutesTest(tests.Test): 'activity_version = 1', 'license = Public Domain', 'stability = stable', - ]])), cmd='submit', initial=True) + ]]), cmd='submit', initial=True) guid3 = 'context3' guid4 = ipc.post(['context'], { 'guid': 'context4', @@ -89,70 +390,722 @@ class RoutesTest(tests.Test): }) self.assertEqual([ - {'guid': guid1, 'title': '1', 'layer': []}, - {'guid': guid2, 'title': '2', 'layer': []}, - {'guid': guid3, 'title': '3', 'layer': []}, - {'guid': guid4, 'title': '4', 'layer': []}, + {'guid': guid1, 'title': '1', 'pins': []}, + {'guid': guid2, 'title': '2', 'pins': []}, + {'guid': guid3, 'title': '3', 'pins': []}, + {'guid': guid4, 'title': '4', 'pins': []}, ], - ipc.get(['context'], reply=['guid', 'title', 'layer'])['result']) + ipc.get(['context'], reply=['guid', 'title', 'pins'])['result']) self.assertEqual([ ], - ipc.get(['context'], reply=['guid', 'title'], layer='favorite')['result']) + ipc.get(['context'], reply=['guid', 'title'], pins='favorite')['result']) self.assertEqual([ ], - ipc.get(['context'], reply=['guid', 'title'], layer='clone')['result']) + ipc.get(['context'], reply=['guid', 'title'], pins='checkin')['result']) ipc.put(['context', guid1], True, cmd='favorite') ipc.put(['context', guid2], True, cmd='favorite') - ipc.put(['context', guid2], True, cmd='clone') - ipc.put(['context', guid3], True, cmd='clone') - self.home_volume['context'].update(guid1, {'title': '1_'}) - self.home_volume['context'].update(guid2, {'title': '2_'}) - self.home_volume['context'].update(guid3, {'title': '3_'}) + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'event': 'checkin', 'state': 'download'}, + {'event': 'checkin', 'state': 'ready'}, + ], + [i for i in ipc.put(['context', guid2], True, cmd='checkin')]) + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'event': 'checkin', 'state': 'download'}, + {'event': 'checkin', 'state': 'ready'}, + ], + [i for i in ipc.put(['context', guid3], True, cmd='checkin')]) + home_volume['context'].update(guid1, {'title': {i18n.default_lang(): '1_'}}) + home_volume['context'].update(guid2, {'title': {i18n.default_lang(): '2_'}}) + home_volume['context'].update(guid3, {'title': {i18n.default_lang(): '3_'}}) self.assertEqual([ - {'guid': guid1, 'title': '1', 'layer': ['favorite']}, - {'guid': guid2, 'title': '2', 'layer': ['clone', 'favorite']}, - {'guid': guid3, 'title': '3', 'layer': ['clone']}, - {'guid': guid4, 'title': '4', 'layer': []}, + {'guid': guid1, 'title': '1', 'pins': ['favorite']}, + {'guid': guid2, 'title': '2', 'pins': ['checkin', 'favorite']}, + {'guid': guid3, 'title': '3', 'pins': ['checkin']}, + {'guid': guid4, 'title': '4', 'pins': []}, ], - ipc.get(['context'], reply=['guid', 'title', 'layer'])['result']) + ipc.get(['context'], reply=['guid', 'title', 'pins'])['result']) self.assertEqual([ {'guid': guid1, 'title': '1_'}, {'guid': guid2, 'title': '2_'}, ], - ipc.get(['context'], reply=['guid', 'title'], layer='favorite')['result']) + ipc.get(['context'], reply=['guid', 'title'], pins='favorite')['result']) self.assertEqual([ {'guid': guid2, 'title': '2_'}, {'guid': guid3, 'title': '3_'}, ], - ipc.get(['context'], reply=['guid', 'title'], layer='clone')['result']) + ipc.get(['context'], reply=['guid', 'title'], pins='checkin')['result']) - def test_SetLocalLayerInOffline(self): - volume = db.Volume('client', model.RESOURCES) - cp = ClientRoutes(volume, client.api.value) - post = Request(method='POST', path=['context']) - post.content_type = 'application/json' - post.content = { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - } + ipc.delete(['context', guid1], cmd='favorite') + ipc.delete(['context', guid2], cmd='checkin') - guid = call(cp, post) - self.assertEqual(['local'], call(cp, Request(method='GET', path=['context', guid, 'layer']))) + self.assertEqual([ + {'guid': guid1, 'pins': []}, + {'guid': guid2, 'pins': ['favorite']}, + {'guid': guid3, 'pins': ['checkin']}, + {'guid': guid4, 'pins': []}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + self.assertEqual([ + {'guid': guid2, 'pins': ['favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='favorite')['result']) + self.assertEqual([ + {'guid': guid3, 'pins': ['checkin']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='checkin')['result']) + + def test_OfflinePins(self): + self.start_online_client() + ipc = IPCConnection() + + ipc.upload(['context'], self.zips(['TestActivity/activity/activity.info', [ + '[Activity]', + 'name = 1', + 'bundle_id = 1', + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ]]), cmd='submit', initial=True) + ipc.upload(['context'], self.zips(['TestActivity/activity/activity.info', [ + '[Activity]', + 'name = 2', + 'bundle_id = 2', + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]]), cmd='submit', initial=True) + ipc.upload(['context'], self.zips(['TestActivity/activity/activity.info', [ + '[Activity]', + 'name = 3', + 'bundle_id = 3', + 'exec = true', + 'icon = icon', + 'activity_version = 3', + 'license = Public Domain', + ]]), cmd='submit', initial=True) + + ipc.put(['context', '1'], None, cmd='favorite') + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'event': 'checkin', 'state': 'download'}, + {'event': 'checkin', 'state': 'ready'}, + ], + [i for i in ipc.put(['context', '2'], None, cmd='checkin')]) + self.assertEqual([ + {'guid': '1', 'pins': ['favorite']}, + {'guid': '2', 'pins': ['checkin']}, + {'guid': '3', 'pins': []}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + + self.assertEqual([ + {'guid': '1', 'pins': ['favorite']}, + {'guid': '2', 'pins': ['checkin']}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + self.assertEqual([ + {'guid': '1', 'pins': ['favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='favorite')['result']) + self.assertEqual([ + {'guid': '2', 'pins': ['checkin']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='checkin')['result']) + + ipc.delete(['context', '1'], cmd='favorite') + ipc.put(['context', '2'], None, cmd='favorite') + self.assertRaises(http.ServiceUnavailable, ipc.put, ['context', '3'], None, cmd='favorite') + + self.assertEqual([ + {'guid': '1', 'pins': []}, + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + self.assertEqual([ + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='favorite')['result']) + self.assertEqual([ + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='checkin')['result']) + + ipc.delete(['context', '2'], cmd='checkin') + ipc.delete(['context', '2'], cmd='favorite') + + self.assertEqual([ + {'guid': '1', 'pins': []}, + {'guid': '2', 'pins': []}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + self.assertEqual([ + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='favorite')['result']) + self.assertEqual([ + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='checkin')['result']) + + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'event': 'failure', 'error': 'Not available in offline', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.put(['context', '1'], None, cmd='checkin')]) + ipc.put(['context', '1'], None, cmd='favorite') + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'event': 'checkin', 'state': 'ready'}, + ], + [i for i in ipc.put(['context', '2'], None, cmd='checkin')]) + ipc.put(['context', '2'], None, cmd='favorite') + self.assertEqual([ + {'event': 'failure', 'error': 'Not available in offline', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.put(['context', '3'], None, cmd='checkin')]) + self.assertRaises(http.ServiceUnavailable, ipc.put, ['context', '3'], None, cmd='favorite') + + self.assertEqual([ + {'guid': '1', 'pins': ['favorite']}, + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'])['result']) + self.assertEqual([ + {'guid': '1', 'pins': ['favorite']}, + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='favorite')['result']) + self.assertEqual([ + {'guid': '2', 'pins': ['checkin', 'favorite']}, + ], + ipc.get(['context'], reply=['guid', 'pins'], pins='checkin')['result']) + + def test_checkin_Notificaitons(self): + self.start_online_client() + ipc = IPCConnection() + + activity_info = '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = context', + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ]) + activity_bundle = self.zips(('topdir/activity/activity.info', activity_info)) + release = ipc.upload(['context'], activity_bundle, cmd='submit', initial=True) + + def subscribe(): + for i in ipc.subscribe(): + if i.get('event') != 'commit': + events.append(i) + events = [] + coroutine.spawn(subscribe) + coroutine.sleep(.1) + del events[:] + + assert {'event': 'checkin', 'state': 'ready'} in [i for i in ipc.put(['context', 'context'], None, cmd='checkin')] + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['inprogress']}, 'resource': 'context'}, + {'event': 'update', 'guid': 'context', 'props': {'pins': ['checkin']}, 'resource': 'context'}, + ], events) + del events[:] + + ipc.put(['context', 'context'], None, cmd='favorite') + ipc.delete(['context', 'context'], cmd='checkin') + coroutine.sleep(.1) + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['favorite']}, 'resource': 'context'}, + ], events) + + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + coroutine.sleep(.1) + del events[:] + + assert {'event': 'checkin', 'state': 'ready'} in [i for i in ipc.put(['context', 'context'], None, cmd='checkin')] + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['favorite', 'inprogress']}, 'resource': 'context'}, + {'event': 'update', 'guid': 'context', 'props': {'pins': ['checkin', 'favorite']}, 'resource': 'context'}, + ], events) + del events[:] + + ipc.delete(['context', 'context'], cmd='checkin') + coroutine.sleep(.1) + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['favorite']}, 'resource': 'context'}, + ], events) + + def test_launch_Notificaitons(self): + self.start_online_client() + ipc = IPCConnection() + + activity_info = '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = context', + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ]) + activity_bundle = self.zips(('topdir/activity/activity.info', activity_info)) + release = ipc.upload(['context'], activity_bundle, cmd='submit', initial=True) + + def subscribe(): + for i in ipc.subscribe(): + if i.get('event') != 'commit': + events.append(i) + events = [] + coroutine.spawn(subscribe) + coroutine.sleep(.1) + del events[:] + + assert {'event': 'launch', 'state': 'exit'} in [i for i in ipc.get(['context', 'context'], cmd='launch')] + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['inprogress']}, 'resource': 'context'}, + {'event': 'update', 'guid': 'context', 'props': {'pins': []}, 'resource': 'context'}, + ], events) + + ipc.put(['context', 'context'], None, cmd='favorite') + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + coroutine.sleep(.1) + del events[:] + + assert {'event': 'launch', 'state': 'exit'} in [i for i in ipc.get(['context', 'context'], None, cmd='launch')] + self.assertEqual([ + {'event': 'update', 'guid': 'context', 'props': {'pins': ['favorite', 'inprogress']}, 'resource': 'context'}, + {'event': 'update', 'guid': 'context', 'props': {'pins': ['favorite']}, 'resource': 'context'}, + ], events) + del events[:] + + def test_checkin_Fails(self): + self.start_online_client() + ipc = IPCConnection() + + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'error': 'Context not found', 'event': 'failure', 'exception': 'NotFound'}, + ], + [i for i in ipc.put(['context', 'context'], None, cmd='checkin')]) + + guid = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'error': 'Failed to solve', 'event': 'failure', 'exception': 'RuntimeError'}, + ], + [i for i in ipc.put(['context', guid], None, cmd='checkin')]) + + ipc.put(['context', guid], None, cmd='favorite') + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + coroutine.sleep(.1) + + self.assertEqual([ + {'error': 'Not available in offline', 'event': 'failure', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.put(['context', 'context'], None, cmd='checkin')]) + + self.assertEqual([ + {'event': 'checkin', 'state': 'solve'}, + {'error': 'Not available in offline', 'event': 'failure', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.put(['context', guid], None, cmd='checkin')]) + + def test_launch_Fails(self): + self.override(injector, '_activity_id_new', lambda: 'activity_id') + self.start_online_client() + ipc = IPCConnection() + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'error': 'Context not found', 'event': 'failure', 'exception': 'NotFound'}, + ], + [i for i in ipc.get(['context', 'context'], cmd='launch')]) + + guid1 = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'error': 'Failed to solve', 'event': 'failure', 'exception': 'RuntimeError'}, + ], + [i for i in ipc.get(['context', guid1], cmd='launch')]) + + activity_info = '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = context2', + 'exec = false', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ]) + activity_bundle = self.zips(('topdir/activity/activity.info', activity_info)) + release = ipc.upload(['context'], activity_bundle, cmd='submit', initial=True) + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'event': 'launch', 'state': 'download'}, + {'event': 'launch', 'state': 'exec'}, + {'context': 'context2', + 'args': ['false', '-b', 'context2', '-a', 'activity_id'], + 'logs': [ + tests.tmpdir + '/.sugar/default/logs/shell.log', + tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', + tests.tmpdir + '/.sugar/default/logs/context2.log', + ], + 'solution': { + 'context2': { + 'blob': release, + 'command': ['activity', 'false'], + 'content-type': 'application/vnd.olpc-sugar', + 'size': len(activity_bundle), + 'title': 'Activity', + 'unpack_size': len(activity_info), + 'version': [[1], 0], + }, + }, + }, + {'error': 'Process exited with 1 status', 'event': 'failure', 'exception': 'RuntimeError'}, + ], + [i for i in ipc.get(['context', 'context2'], cmd='launch')]) + + ipc.put(['context', guid1], None, cmd='favorite') + ipc.put(['context', 'context2'], None, cmd='favorite') + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + coroutine.sleep(.1) + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'error': 'Not available in offline', 'event': 'failure', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.get(['context', 'context'], cmd='launch')]) + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'error': 'Not available in offline', 'event': 'failure', 'exception': 'ServiceUnavailable'}, + ], + [i for i in ipc.get(['context', guid1], cmd='launch')]) + + self.assertEqual([ + {'activity_id': 'activity_id'}, + {'event': 'launch', 'state': 'init'}, + {'event': 'launch', 'state': 'solve'}, + {'event': 'launch', 'state': 'exec'}, + {'context': 'context2', + 'args': ['false', '-b', 'context2', '-a', 'activity_id'], + 'logs': [ + tests.tmpdir + '/.sugar/default/logs/shell.log', + tests.tmpdir + '/.sugar/default/logs/sugar-network-client.log', + tests.tmpdir + '/.sugar/default/logs/context2_1.log', + ], + 'solution': { + 'context2': { + 'blob': release, + 'command': ['activity', 'false'], + 'content-type': 'application/vnd.olpc-sugar', + 'size': len(activity_bundle), + 'title': 'Activity', + 'unpack_size': len(activity_info), + 'version': [[1], 0], + }, + }, + }, + {'error': 'Process exited with 1 status', 'event': 'failure', 'exception': 'RuntimeError'}, + ], + [i for i in ipc.get(['context', 'context2'], cmd='launch')]) + + def test_SubmitReport(self): + home_volume = self.start_online_client() + ipc = IPCConnection() + + self.touch( + ['file1', 'content1'], + ['file2', 'content2'], + ['file3', 'content3'], + ) + events = [i for i in ipc.post(['report'], {'context': 'context', 'error': 'error', 'logs': [ + tests.tmpdir + '/file1', + tests.tmpdir + '/file2', + tests.tmpdir + '/file3', + ]}, cmd='submit')] + self.assertEqual('done', events[-1]['event']) + guid = events[-1]['guid'] + + self.assertEqual({ + 'context': 'context', + 'error': 'error', + }, + ipc.get(['report', guid], reply=['context', 'error'])) + self.assertEqual(sorted([ + 'content1', + 'content2', + 'content3', + ]), + sorted([ipc.get(['report', guid, 'logs', i]) for i in ipc.get(['report', guid, 'logs']).keys()])) + assert not home_volume['report'][guid].exists + + self.stop_master() + self.wait_for_events(event='inline', state='offline').wait() + + events = [i for i in ipc.post(['report'], {'context': 'context', 'error': 'error', 'logs': [ + tests.tmpdir + '/file1', + tests.tmpdir + '/file2', + tests.tmpdir + '/file3', + ]}, cmd='submit')] + self.assertEqual('done', events[-1]['event']) + guid = events[-1]['guid'] + + self.assertEqual({ + 'context': 'context', + 'error': 'error', + }, + ipc.get(['report', guid], reply=['context', 'error'])) + self.assertEqual(sorted([ + 'content1', + 'content2', + 'content3', + ]), + sorted([ipc.get(['report', guid, 'logs', i]) for i in ipc.get(['report', guid, 'logs']).keys()])) + assert home_volume['report'][guid].exists + + def test_inline(self): + routes._RECONNECT_TIMEOUT = 2 + + this.injector = Injector('client') + cp = ClientRoutes(db.Volume('client', RESOURCES)) + cp.connect(client.api.value) + assert not cp.inline() trigger = self.wait_for_events(cp, event='inline', state='online') - node_volume = self.start_master() - cp._remote_connect() + coroutine.sleep(.5) + self.fork_master() + trigger.wait(.5) + assert trigger.value is None + assert not cp.inline() + trigger.wait() + assert cp.inline() + + trigger = self.wait_for_events(cp, event='inline', state='offline') + self.stop_master() + trigger.wait() + assert not cp.inline() + + def test_DoNotSwitchToOfflineOnRedirectFails(self): + + class Document(db.Resource): + + @db.stored_property(db.Blob) + def blob1(self, value): + raise http.Redirect(prefix + '/blob2') + + @db.stored_property(db.Blob) + def blob2(self, value): + raise http._ConnectionError() + + local_volume = self.start_online_client([User, Document]) + ipc = IPCConnection() + guid = ipc.post(['document'], {}) + prefix = client.api.value + '/document/' + guid + '/' + local_volume['document'].create({'guid': guid}) + + trigger = self.wait_for_events(ipc, event='inline', state='connecting') + try: + ipc.get(['document', guid, 'blob1']) + except Exception: + pass + assert trigger.wait(.1) is None + + trigger = self.wait_for_events(ipc, event='inline', state='connecting') + try: + ipc.get(['document', guid, 'blob2']) + except Exception: + pass + assert trigger.wait(.1) is not None + + def test_FallbackToLocalOnRemoteTransportFails(self): + + class LocalRoutes(routes._LocalRoutes): + + @route('GET', cmd='sleep') + def sleep(self): + return 'local' + + @route('GET', cmd='yield_raw_and_sleep', + mime_type='application/octet-stream') + def yield_raw_and_sleep(self): + yield 'local' + + @route('GET', cmd='yield_json_and_sleep', + mime_type='application/json') + def yield_json_and_sleep(self): + yield '"local"' + + self.override(routes, '_LocalRoutes', LocalRoutes) + this.injector = Injector('client') + home_volume = self.start_client() + ipc = IPCConnection() + + self.assertEqual('local', ipc.get(cmd='sleep')) + self.assertEqual('local', ipc.get(cmd='yield_raw_and_sleep')) + self.assertEqual('local', ipc.get(cmd='yield_json_and_sleep')) + + class NodeRoutes(MasterRoutes): + + @route('GET', cmd='sleep') + def sleep(self): + coroutine.sleep(.5) + return 'remote' + + @route('GET', cmd='yield_raw_and_sleep', + mime_type='application/octet-stream') + def yield_raw_and_sleep(self): + for __ in range(33): + yield "remote\n" + coroutine.sleep(.5) + for __ in range(33): + yield "remote\n" + + @route('GET', cmd='yield_json_and_sleep', + mime_type='application/json') + def yield_json_and_sleep(self): + yield '"' + yield 'r' + coroutine.sleep(1) + yield 'emote"' + + node_pid = self.fork_master([User], NodeRoutes) + self.client_routes._remote_connect() + self.wait_for_events(ipc, event='inline', state='online').wait() + + ts = time.time() + self.assertEqual('remote', ipc.get(cmd='sleep')) + self.assertEqual('remote\n' * 66, ipc.get(cmd='yield_raw_and_sleep')) + self.assertEqual('remote', ipc.get(cmd='yield_json_and_sleep')) + assert time.time() - ts >= 2 + + def kill(): + coroutine.sleep(.5) + self.waitpid(node_pid) + + coroutine.spawn(kill) + self.assertEqual('local', ipc.get(cmd='sleep')) + assert not ipc.get(cmd='inline') + + node_pid = self.fork_master([User], NodeRoutes) + self.client_routes._remote_connect() + self.wait_for_events(ipc, event='inline', state='online').wait() + + coroutine.spawn(kill) + self.assertEqual('local', ipc.get(cmd='yield_raw_and_sleep')) + assert not ipc.get(cmd='inline') + + node_pid = self.fork_master([User], NodeRoutes) + self.client_routes._remote_connect() + self.wait_for_events(ipc, event='inline', state='online').wait() + + coroutine.spawn(kill) + self.assertEqual('local', ipc.get(cmd='yield_json_and_sleep')) + assert not ipc.get(cmd='inline') + + def test_ReconnectOnServerFall(self): + routes._RECONNECT_TIMEOUT = 1 + + this.injector = Injector('client') + node_pid = self.fork_master() + self.start_client() + ipc = IPCConnection() + self.wait_for_events(ipc, event='inline', state='online').wait() + + def shutdown(): + coroutine.sleep(.1) + self.waitpid(node_pid) + coroutine.spawn(shutdown) + self.wait_for_events(ipc, event='inline', state='offline').wait() + + self.fork_master() + self.wait_for_events(ipc, event='inline', state='online').wait() + + def test_SilentReconnectOnGatewayErrors(self): + + class Routes(object): + + subscribe_tries = 0 + + def __init__(self, volume, *args): + pass + + @route('GET', cmd='status', mime_type='application/json') + def info(self): + return {'resources': {}} + + @route('GET', cmd='subscribe', mime_type='text/event-stream') + def subscribe(self, request=None, response=None, **condition): + Routes.subscribe_tries += 1 + coroutine.sleep(.1) + if Routes.subscribe_tries % 2: + raise http.BadGateway() + else: + raise http.GatewayTimeout() + + this.injector = Injector('client') + node_pid = self.start_master(None, Routes) + self.start_client() + ipc = IPCConnection() + self.wait_for_events(ipc, event='inline', state='online').wait() + + def read_events(): + for event in ipc.subscribe(): + events.append(event) + events = [] + coroutine.spawn(read_events) + + coroutine.sleep(1) + self.assertEqual([{'event': 'pong'}], events) + assert Routes.subscribe_tries > 2 + - guid = call(cp, post) - self.assertEqual([], call(cp, Request(method='GET', path=['context', guid, 'layer']))) - def test_CachedClientRoutes(self): - volume = db.Volume('client', model.RESOURCES, lazy_open=True) + + + + + + + def ___test_CachedClientRoutes(self): + volume = db.Volume('client', RESOURCES, lazy_open=True) cp = CachedClientRoutes(volume, client.api.value) post = Request(method='POST', path=['context']) @@ -214,8 +1167,8 @@ class RoutesTest(tests.Test): {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}, self.node_volume['context'].get(guid2)['author']) - def test_CachedClientRoutes_WipeReports(self): - volume = db.Volume('client', model.RESOURCES, lazy_open=True) + def ___test_CachedClientRoutes_WipeReports(self): + volume = db.Volume('client', RESOURCES, lazy_open=True) cp = CachedClientRoutes(volume, client.api.value) post = Request(method='POST', path=['report']) @@ -227,15 +1180,15 @@ class RoutesTest(tests.Test): guid = call(cp, post) trigger = self.wait_for_events(cp, event='push') - self.start_master([User, Report]) + self.start_master() cp._remote_connect() trigger.wait() assert not volume['report'].exists(guid) assert self.node_volume['report'].exists(guid) - def test_CachedClientRoutes_OpenOnlyChangedResources(self): - volume = db.Volume('client', model.RESOURCES, lazy_open=True) + def ___test_CachedClientRoutes_OpenOnlyChangedResources(self): + volume = db.Volume('client', RESOURCES, lazy_open=True) cp = CachedClientRoutes(volume, client.api.value) guid = call(cp, Request(method='POST', path=['context'], content_type='application/json', content={ 'type': 'activity', @@ -246,7 +1199,7 @@ class RoutesTest(tests.Test): })) cp.close() - volume = db.Volume('client', model.RESOURCES, lazy_open=True) + volume = db.Volume('client', RESOURCES, lazy_open=True) cp = CachedClientRoutes(volume, client.api.value) trigger = self.wait_for_events(cp, event='push') @@ -258,8 +1211,8 @@ class RoutesTest(tests.Test): assert self.node_volume['context'].exists(guid) self.assertEqual(['context'], volume.keys()) - def test_SwitchToOfflineForAbsentOnlineProps(self): - volume = db.Volume('client', model.RESOURCES) + def ___test_SwitchToOfflineForAbsentOnlineProps(self): + volume = db.Volume('client', RESOURCES) cp = ClientRoutes(volume, client.api.value) post = Request(method='POST', path=['context']) @@ -282,177 +1235,6 @@ class RoutesTest(tests.Test): assert not self.node_volume['context'].exists(guid) self.assertEqual('title', call(cp, Request(method='GET', path=['context', guid, 'title']))) - def test_I18nQuery(self): - os.environ['LANGUAGE'] = 'foo' - self.start_online_client() - ipc = IPCConnection() - - guid1 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en-US': 'qwe', 'ru-RU': 'йцу'}, - 'summary': 'summary', - 'description': 'description', - }) - guid2 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en-US': 'qwerty', 'ru-RU': 'йцукен'}, - 'summary': 'summary', - 'description': 'description', - }) - - self.assertEqual([ - {'guid': guid1}, - {'guid': guid2}, - ], - ipc.get(['context'], query='йцу')['result']) - self.assertEqual([ - {'guid': guid1}, - {'guid': guid2}, - ], - ipc.get(['context'], query='qwe')['result']) - - self.assertEqual([ - {'guid': guid2}, - ], - ipc.get(['context'], query='йцукен')['result']) - self.assertEqual([ - {'guid': guid2}, - ], - ipc.get(['context'], query='qwerty')['result']) - - def test_IgnoreClonesOnOpen(self): - self.start_online_client() - ipc = IPCConnection() - - guid = ipc.upload(['release'], StringIO(self.zips(['TestActivity/activity/activity.info', [ - '[Activity]', - 'name = name', - 'bundle_id = context', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - 'stability = stable', - ]])), cmd='submit', initial=True) - ipc.put(['context', 'context'], True, cmd='clone') - ts = time.time() - os.utime('client/release/%s/%s' % (guid[:2], guid), (ts - 2 * 86400, ts - 2 * 86400)) - self.client_routes.close() - self.stop_nodes() - - home_volume = self.start_online_client() - cache_lifetime.value = 1 - self.client_routes.recycle() - assert home_volume['release'].exists(guid) - assert exists('client/release/%s/%s' % (guid[:2], guid)) - - def test_IgnoreClonesWhileCheckingFreeSpace(self): - home_volume = self.start_online_client() - ipc = IPCConnection() - - guid = ipc.upload(['release'], StringIO(self.zips(['TestActivity/activity/activity.info', [ - '[Activity]', - 'name = name', - 'bundle_id = context', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - 'stability = stable', - ]])), cmd='submit', initial=True) - ipc.put(['context', 'context'], True, cmd='clone') - - class statvfs(object): - f_blocks = 100 - f_bfree = 10 - f_frsize = 1 - - self.override(os, 'statvfs', lambda *args: statvfs()) - cache_limit.value = 10 - - self.assertRaises(RuntimeError, self.client_routes._cache.ensure, 1, 0) - assert home_volume['release'].exists(guid) - assert exists('client/release/%s/%s' % (guid[:2], guid)) - - def test_IgnoreClonesOnRecycle(self): - home_volume = self.start_online_client() - ipc = IPCConnection() - - guid = ipc.upload(['release'], StringIO(self.zips(['TestActivity/activity/activity.info', [ - '[Activity]', - 'name = name', - 'bundle_id = context', - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - 'stability = stable', - ]])), cmd='submit', initial=True) - ipc.put(['context', 'context'], True, cmd='clone') - ts = time.time() - os.utime('client/release/%s/%s' % (guid[:2], guid), (ts - 2 * 86400, ts - 2 * 86400)) - - cache_lifetime.value = 1 - self.client_routes.recycle() - assert home_volume['release'].exists(guid) - assert exists('client/release/%s/%s' % (guid[:2], guid)) - - def test_LanguagesFallbackInRequests(self): - self.start_online_client() - ipc = IPCConnection() - - guid1 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1', 'ru': '2', 'es': '3'}, - 'summary': '', - 'description': '', - }) - guid2 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1', 'ru': '2'}, - 'summary': '', - 'description': '', - }) - guid3 = self.node_volume['context'].create({ - 'type': 'activity', - 'title': {'en': '1'}, - 'summary': '', - 'description': '', - }) - - i18n._default_langs = None - os.environ['LANGUAGE'] = 'es:ru:en' - ipc = IPCConnection() - self.assertEqual('3', ipc.get(['context', guid1, 'title'])) - self.assertEqual('2', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - i18n._default_langs = None - os.environ['LANGUAGE'] = 'ru:en' - ipc = IPCConnection() - self.assertEqual('2', ipc.get(['context', guid1, 'title'])) - self.assertEqual('2', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - i18n._default_langs = None - os.environ['LANGUAGE'] = 'en' - ipc = IPCConnection() - self.assertEqual('1', ipc.get(['context', guid1, 'title'])) - self.assertEqual('1', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - i18n._default_langs = None - os.environ['LANGUAGE'] = 'foo' - ipc = IPCConnection() - self.assertEqual('1', ipc.get(['context', guid1, 'title'])) - self.assertEqual('1', ipc.get(['context', guid2, 'title'])) - self.assertEqual('1', ipc.get(['context', guid3, 'title'])) - - -def call(routes, request): - router = Router(routes) - return router.call(request, Response()) - if __name__ == '__main__': tests.main() diff --git a/tests/units/client/server_routes.py b/tests/units/client/server_routes.py deleted file mode 100755 index 8c72468..0000000 --- a/tests/units/client/server_routes.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python -# sugar-lint: disable - -import os -import shutil -import hashlib -from os.path import exists - -from __init__ import tests, src_root - -from sugar_network import db, client, model -from sugar_network.client import IPCConnection -from sugar_network.client.routes import ClientRoutes -from sugar_network.db import Volume -from sugar_network.toolkit.router import Router -from sugar_network.toolkit import mountpoints, coroutine - - -class ServerRoutesTest(tests.Test): - - def test_whoami(self): - self.start_node() - ipc = IPCConnection() - - self.assertEqual( - {'guid': tests.UID, 'roles': [], 'route': 'proxy'}, - ipc.get(cmd='whoami')) - - def test_Events(self): - self.start_node() - ipc = IPCConnection() - events = [] - - def read_events(): - for event in ipc.subscribe(event='!commit'): - events.append(event) - coroutine.spawn(read_events) - coroutine.dispatch() - - guid = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - ipc.put(['context', guid], { - 'title': 'title_2', - }) - coroutine.sleep(.1) - ipc.delete(['context', guid]) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - guid = self.node_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.node_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - self.node_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - guid = self.home_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.home_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - self.home_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([], events) - return - - self.node.stop() - coroutine.sleep(.1) - del events[:] - - guid = self.home_volume['context'].create({ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - self.home_volume['context'].update(guid, { - 'title': 'title_2', - }) - coroutine.sleep(.1) - self.home_volume['context'].delete(guid) - coroutine.sleep(.1) - - self.assertEqual([ - {'guid': guid, 'resource': 'context', 'event': 'create'}, - {'guid': guid, 'resource': 'context', 'event': 'update'}, - {'guid': guid, 'event': 'delete', 'resource': 'context'}, - ], - events) - del events[:] - - def test_BLOBs(self): - self.start_node() - ipc = IPCConnection() - - guid = ipc.post(['context'], { - 'type': 'package', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - blob = 'logo_blob' - ipc.request('PUT', ['context', guid, 'logo'], blob) - - self.assertEqual( - blob, - ipc.request('GET', ['context', guid, 'logo']).content) - self.assertEqual({ - 'logo': { - 'url': 'http://127.0.0.1:5555/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['logo'])) - self.assertEqual([{ - 'logo': { - 'url': 'http://127.0.0.1:5555/context/%s/logo' % guid, - 'blob_size': len(blob), - 'digest': hashlib.sha1(blob).hexdigest(), - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['logo'])['result']) - - self.assertEqual( - file(src_root + '/sugar_network/static/httpdocs/images/package.png').read(), - ipc.request('GET', ['context', guid, 'icon']).content) - self.assertEqual({ - 'icon': { - 'url': 'http://127.0.0.1:5555/static/images/package.png', - 'mime_type': 'image/png', - }, - }, - ipc.get(['context', guid], reply=['icon'])) - self.assertEqual([{ - 'icon': { - 'url': 'http://127.0.0.1:5555/static/images/package.png', - 'mime_type': 'image/png', - }, - }], - ipc.get(['context'], reply=['icon'])['result']) - - def test_PopulateNode(self): - os.makedirs('disk/sugar-network') - volume = Volume('db', model.RESOURCES) - cp = ClientRoutes(volume) - - assert not cp.inline() - trigger = self.wait_for_events(cp, event='inline', state='online') - mountpoints.populate('.') - coroutine.dispatch() - assert trigger.value is not None - assert cp.inline() - - def test_MountNode(self): - volume = Volume('db', model.RESOURCES) - cp = ClientRoutes(volume) - - trigger = self.wait_for_events(cp, event='inline', state='online') - mountpoints.populate('.') - assert not cp.inline() - assert trigger.value is None - - coroutine.spawn(mountpoints.monitor, '.') - coroutine.dispatch() - os.makedirs('disk/sugar-network') - trigger.wait() - assert cp.inline() - - def test_UnmountNode(self): - cp = self.start_node() - assert cp.inline() - trigger = self.wait_for_events(cp, event='inline', state='offline') - shutil.rmtree('disk') - trigger.wait() - assert not cp.inline() - - def start_node(self): - os.makedirs('disk/sugar-network') - self.home_volume = Volume('db', model.RESOURCES) - cp = ClientRoutes(self.home_volume) - trigger = self.wait_for_events(cp, event='inline', state='online') - coroutine.spawn(mountpoints.monitor, tests.tmpdir) - trigger.wait() - self.node_volume = cp._node.volume - server = coroutine.WSGIServer(('127.0.0.1', client.ipc_port.value), Router(cp)) - coroutine.spawn(server.serve_forever) - coroutine.dispatch() - return cp - - -if __name__ == '__main__': - tests.main() diff --git a/tests/units/db/resource.py b/tests/units/db/resource.py index 64187aa..05aaddf 100755 --- a/tests/units/db/resource.py +++ b/tests/units/db/resource.py @@ -31,6 +31,7 @@ class ResourceTest(tests.Test): def setUp(self, fork_num=0): tests.Test.setUp(self, fork_num) + this.localcast = lambda x: x this.broadcast = lambda x: x def test_ActiveProperty_Slotted(self): @@ -45,7 +46,7 @@ class ResourceTest(tests.Test): def not_slotted(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) self.assertEqual(1, directory.metadata['slotted'].slot) directory.create({'slotted': 'slotted', 'not_slotted': 'not_slotted'}) @@ -70,7 +71,7 @@ class ResourceTest(tests.Test): def prop_2(self, value): return value - self.assertRaises(RuntimeError, Directory, tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + self.assertRaises(RuntimeError, Directory, tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) def test_ActiveProperty_Terms(self): @@ -84,7 +85,7 @@ class ResourceTest(tests.Test): def not_term(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) self.assertEqual('T', directory.metadata['term'].prefix) guid = directory.create({'term': 'term', 'not_term': 'not_term'}) @@ -110,7 +111,7 @@ class ResourceTest(tests.Test): def prop_2(self, value): return value - self.assertRaises(RuntimeError, Directory, tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + self.assertRaises(RuntimeError, Directory, tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) def test_ActiveProperty_FullTextSearch(self): @@ -124,7 +125,7 @@ class ResourceTest(tests.Test): def yes(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) self.assertEqual(False, directory.metadata['no'].full_text) self.assertEqual(True, directory.metadata['yes'].full_text) @@ -145,7 +146,7 @@ class ResourceTest(tests.Test): def prop_2(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'prop_1': '1', 'prop_2': '2'}) self.assertEqual( @@ -165,7 +166,7 @@ class ResourceTest(tests.Test): def prop(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid_1 = directory.create({'prop': '1'}) guid_2 = directory.create({'prop': '2'}) @@ -212,7 +213,7 @@ class ResourceTest(tests.Test): ('db/document/2/2/seqno', '{"value": 0}'), ) - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) self.assertEqual(0, directory._index.mtime) for i in directory.populate(): @@ -264,7 +265,7 @@ class ResourceTest(tests.Test): ('db/document/3/3/seqno', ''), ) - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) populated = 0 for i in directory.populate(): @@ -285,7 +286,7 @@ class ResourceTest(tests.Test): def prop(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': 'guid', 'prop': 'foo'}) self.assertEqual( @@ -305,7 +306,7 @@ class ResourceTest(tests.Test): def prop(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid_1 = directory.create({'prop': 'value'}) seqno = directory.get(guid_1).get('seqno') @@ -349,7 +350,7 @@ class ResourceTest(tests.Test): def prop2(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': '1', 'prop1': '1', 'prop2': '2'}) doc = directory.get(guid) @@ -366,7 +367,7 @@ class ResourceTest(tests.Test): def prop(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': '1', 'prop': {'ru': 'ru'}}) doc = directory.get(guid) @@ -384,7 +385,7 @@ class ResourceTest(tests.Test): def prop(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'prop': '1'}) self.assertEqual([guid], [i.guid for i in directory.find()[0]]) directory.commit() @@ -417,7 +418,7 @@ class ResourceTest(tests.Test): def prop3(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': 'guid', 'prop1': 'set1', 'prop2': 'set2', 'prop3': 'set3'}) doc = directory.get(guid) @@ -446,7 +447,7 @@ class ResourceTest(tests.Test): def prop3(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': 'guid', 'prop2': 'set2'}) doc = directory.get(guid) @@ -475,7 +476,7 @@ class ResourceTest(tests.Test): def prop3(self, value): return value - directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno()) + directory = Directory(tests.tmpdir, Document, IndexWriter, _SessionSeqno(), this.broadcast) guid = directory.create({'guid': 'guid', 'prop2': 'set2'}) doc = directory.get(guid) diff --git a/tests/units/db/routes.py b/tests/units/db/routes.py index 9f9131e..4fa4cef 100755 --- a/tests/units/db/routes.py +++ b/tests/units/db/routes.py @@ -24,6 +24,10 @@ from sugar_network.toolkit import coroutine, http, i18n class RoutesTest(tests.Test): + def setUp(self, fork_num=0): + tests.Test.setUp(self, fork_num) + this.localcast = lambda x: x + def test_PostDefaults(self): class Document(db.Resource): @@ -646,12 +650,6 @@ class RoutesTest(tests.Test): def test_on_create_Override(self): - class Routes(db.Routes): - - def on_create(self, request, props): - props['prop'] = 'overriden' - db.Routes.on_create(self, request, props) - class TestDocument(db.Resource): @db.indexed_property(slot=1, default='') @@ -662,13 +660,16 @@ class RoutesTest(tests.Test): def localized_prop(self, value): return value + def created(self): + self.posts['prop'] = 'overriden' + volume = db.Volume(tests.tmpdir, [TestDocument]) - router = Router(Routes(volume)) + router = Router(db.Routes(volume)) - guid = this.call(method='POST', path=['testdocument'], content={'prop': 'foo'}, routes=Routes) + guid = this.call(method='POST', path=['testdocument'], content={'prop': 'foo'}) self.assertEqual('overriden', volume['testdocument'].get(guid)['prop']) - this.call(method='PUT', path=['testdocument', guid], content={'prop': 'bar'}, routes=Routes) + this.call(method='PUT', path=['testdocument', guid], content={'prop': 'bar'}) self.assertEqual('bar', volume['testdocument'].get(guid)['prop']) def test_on_update(self): @@ -696,12 +697,6 @@ class RoutesTest(tests.Test): def test_on_update_Override(self): - class Routes(db.Routes): - - def on_update(self, request, props): - props['prop'] = 'overriden' - db.Routes.on_update(self, request, props) - class TestDocument(db.Resource): @db.indexed_property(slot=1, default='') @@ -712,13 +707,16 @@ class RoutesTest(tests.Test): def localized_prop(self, value): return value + def updated(self): + self.posts['prop'] = 'overriden' + volume = db.Volume(tests.tmpdir, [TestDocument]) - router = Router(Routes(volume)) + router = Router(db.Routes(volume)) - guid = this.call(method='POST', path=['testdocument'], content={'prop': 'foo'}, routes=Routes) + guid = this.call(method='POST', path=['testdocument'], content={'prop': 'foo'}) self.assertEqual('foo', volume['testdocument'].get(guid)['prop']) - this.call(method='PUT', path=['testdocument', guid], content={'prop': 'bar'}, routes=Routes) + this.call(method='PUT', path=['testdocument', guid], content={'prop': 'bar'}) self.assertEqual('overriden', volume['testdocument'].get(guid)['prop']) def __test_DoNotPassGuidsForCreate(self): @@ -796,6 +794,7 @@ class RoutesTest(tests.Test): ) events = [] + this.localcast = lambda x: events.append(x) this.broadcast = lambda x: events.append(x) volume = db.Volume(tests.tmpdir, [Document1, Document2]) volume['document1'] @@ -821,8 +820,8 @@ class RoutesTest(tests.Test): volume['document1'].update('guid1', {'prop': 'foo'}) volume['document2'].update('guid2', {'prop': 'bar'}) self.assertEqual([ - {'event': 'update', 'resource': 'document1', 'guid': 'guid1'}, - {'event': 'update', 'resource': 'document2', 'guid': 'guid2'}, + {'event': 'update', 'resource': 'document1', 'guid': 'guid1', 'props': {'prop': 'foo'}}, + {'event': 'update', 'resource': 'document2', 'guid': 'guid2', 'props': {'prop': 'bar'}}, ], events) del events[:] @@ -1516,7 +1515,7 @@ class RoutesTest(tests.Test): events = [] volume = db.Volume(tests.tmpdir, [Document]) router = Router(db.Routes(volume)) - this.broadcast = lambda x: events.append(x) + this.localcast = lambda x: events.append(x) guid = this.call(method='POST', path=['document'], content={}) self.assertRaises(http.NotFound, this.call, method='POST', path=['document', 'foo', 'bar'], content={}) @@ -1532,7 +1531,10 @@ class RoutesTest(tests.Test): }, volume['document'].get(guid)['prop3']) self.assertEqual([ - {'event': 'update', 'resource': 'document', 'guid': guid}, + {'event': 'update', 'resource': 'document', 'guid': guid, 'props': { + 'mtime': 0, + 'prop3': {'0': {'seqno': 2, 'value': 0}}, + }}, ], events) @@ -1556,6 +1558,7 @@ class RoutesTest(tests.Test): volume['document'].get(guid)['prop3']) def test_RemoveAggprops(self): + self.override(time, 'time', lambda: 0) class Document(db.Resource): @@ -1570,7 +1573,7 @@ class RoutesTest(tests.Test): events = [] volume = db.Volume(tests.tmpdir, [Document]) router = Router(db.Routes(volume)) - this.broadcast = lambda x: events.append(x) + this.localcast = lambda x: events.append(x) guid = this.call(method='POST', path=['document'], content={}) agg_guid = this.call(method='POST', path=['document', guid, 'prop1'], content=2) @@ -1594,7 +1597,10 @@ class RoutesTest(tests.Test): {agg_guid: {'seqno': 4}}, volume['document'].get(guid)['prop2']) self.assertEqual([ - {'event': 'update', 'resource': 'document', 'guid': guid}, + {'event': 'update', 'resource': 'document', 'guid': guid, 'props': { + 'mtime': 0, + 'prop2': {agg_guid: {'seqno': 4}}, + }}, ], events) @@ -1609,7 +1615,7 @@ class RoutesTest(tests.Test): events = [] volume = db.Volume(tests.tmpdir, [Document]) router = Router(db.Routes(volume)) - this.broadcast = lambda x: events.append(x) + this.localcast = lambda x: events.append(x) guid = this.call(method='POST', path=['document'], content={}) del events[:] @@ -1617,6 +1623,7 @@ class RoutesTest(tests.Test): self.assertEqual([], events) def test_UpdateAggprops(self): + self.override(time, 'time', lambda: 0) class Document(db.Resource): @@ -1631,7 +1638,7 @@ class RoutesTest(tests.Test): events = [] volume = db.Volume(tests.tmpdir, [Document]) router = Router(db.Routes(volume)) - this.broadcast = lambda x: events.append(x) + this.localcast = lambda x: events.append(x) guid = this.call(method='POST', path=['document'], content={}) agg_guid = this.call(method='POST', path=['document', guid, 'prop1'], content=1) @@ -1655,11 +1662,15 @@ class RoutesTest(tests.Test): {agg_guid: {'seqno': 4, 'value': 3}}, volume['document'].get(guid)['prop2']) self.assertEqual([ - {'event': 'update', 'resource': 'document', 'guid': guid}, + {'event': 'update', 'resource': 'document', 'guid': guid, 'props': { + 'mtime': 0, + 'prop2': {agg_guid: {'seqno': 4, 'value': 3}}, + }}, ], events) def test_PostAbsentAggpropsOnUpdate(self): + self.override(time, 'time', lambda: 0) class Document(db.Resource): @@ -1670,7 +1681,7 @@ class RoutesTest(tests.Test): events = [] volume = db.Volume(tests.tmpdir, [Document]) router = Router(db.Routes(volume)) - this.broadcast = lambda x: events.append(x) + this.localcast = lambda x: events.append(x) guid = this.call(method='POST', path=['document'], content={}) del events[:] @@ -1679,7 +1690,10 @@ class RoutesTest(tests.Test): {'absent': {'seqno': 2, 'value': 'probe'}}, volume['document'].get(guid)['prop']) self.assertEqual([ - {'event': 'update', 'resource': 'document', 'guid': guid}, + {'event': 'update', 'resource': 'document', 'guid': guid, 'props': { + 'mtime': 0, + 'prop': {'absent': {'seqno': 2, 'value': 'probe'}}, + }}, ], events) @@ -1824,6 +1838,27 @@ class RoutesTest(tests.Test): sorted([guid2, guid3]), sorted([i['guid'] for i in this.call(method='GET', path=['document'], query='comments:c')['result']])) + def test_HandleDeletes(self): + + class Document(db.Resource): + pass + + volume = db.Volume(tests.tmpdir, [Document]) + router = Router(db.Routes(volume)) + + guid = this.call(method='POST', path=['document'], content={}) + self.assertEqual('active', volume['document'][guid]['state']) + + events = [] + this.localcast = lambda x: events.append(x) + this.call(method='DELETE', path=['document', guid], principal=tests.UID) + + self.assertRaises(http.NotFound, this.call, method='GET', path=['document', guid]) + self.assertEqual('deleted', volume['document'][guid]['state']) + self.assertEqual( + [{'event': 'delete', 'resource': 'document', 'guid': guid}], + events) + if __name__ == '__main__': tests.main() diff --git a/tests/units/db/volume.py b/tests/units/db/volume.py index 3b10d7e..b5f01a7 100755 --- a/tests/units/db/volume.py +++ b/tests/units/db/volume.py @@ -31,7 +31,7 @@ class VolumeTest(tests.Test): def setUp(self, fork_num=0): tests.Test.setUp(self, fork_num) - this.broadcast = lambda x: x + this.localcast = lambda x: x def test_diff(self): @@ -323,6 +323,51 @@ class VolumeTest(tests.Test): self.assertRaises(StopIteration, patch.next) self.assertEqual([[4, None]], r) + def test_clone(self): + + class Document(db.Resource): + + @db.stored_property() + def prop1(self, value): + return value + + @db.stored_property() + def prop2(self, value): + return value + + @db.stored_property(db.Blob) + def prop3(self, value): + return value + + @db.stored_property(db.Blob) + def prop4(self, value): + return value + + volume = db.Volume('.', [Document]) + + volume['document'].create({ + 'guid': 'guid', + 'prop1': '1', + 'prop2': 2, + 'prop3': volume.blobs.post('333', '3/3').digest, + 'prop4': volume.blobs.post('4444', '4/4').digest, + }) + self.utime('db/document/gu/guid', 1) + + self.assertEqual([ + {'content-type': '3/3', 'content-length': '3', 'x-seqno': '1'}, + {'content-type': '4/4', 'content-length': '4', 'x-seqno': '2'}, + {'resource': 'document'}, + {'guid': 'guid', 'patch': { + 'guid': {'value': 'guid', 'mtime': 1}, + 'prop1': {'value': '1', 'mtime': 1}, + 'prop2': {'value': 2, 'mtime': 1}, + 'prop3': {'value': hashlib.sha1('333').hexdigest(), 'mtime': 1}, + 'prop4': {'value': hashlib.sha1('4444').hexdigest(), 'mtime': 1}, + }}, + ], + [dict(i) for i in volume.clone('document', 'guid')]) + def test_patch_New(self): class Document(db.Resource): @@ -735,7 +780,7 @@ class VolumeTest(tests.Test): def prop(self, value): return value + 1 - directory = Directory('document', Document, IndexWriter, _SessionSeqno()) + directory = Directory('document', Document, IndexWriter, _SessionSeqno(), this.localcast) directory.patch('1', { 'guid': {'mtime': 1, 'value': '1'}, @@ -772,7 +817,7 @@ class VolumeTest(tests.Test): patch = generator() self.assertEqual((101, [[1, 3]]), volume.patch(patch)) - assert volume['document'].exists('1') + assert volume['document']['1'].exists class _SessionSeqno(object): diff --git a/tests/units/model/context.py b/tests/units/model/context.py index bd39c04..45a1ce8 100755 --- a/tests/units/model/context.py +++ b/tests/units/model/context.py @@ -96,20 +96,20 @@ class ContextTest(tests.Test): assert release1 == str(hashlib.sha1(bundle1).hexdigest()) self.assertEqual({ release1: { - 'seqno': 5, + 'seqno': 10, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:1')[0]).guid, 'version': [[1], 0], 'requires': {}, - 'command': 'true', + 'commands': {'activity': {'exec': 'true'}}, 'bundles': {'*-*': {'blob': str(hashlib.sha1(bundle1).hexdigest()), 'unpack_size': len(activity_info1)}}, 'stability': 'stable', }, }, }, conn.get(['context', context, 'releases'])) - assert blobs.get(str(hashlib.sha1(bundle1).hexdigest())) + assert volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists activity_info2 = '\n'.join([ '[Activity]', @@ -125,71 +125,71 @@ class ContextTest(tests.Test): assert release2 == str(hashlib.sha1(bundle2).hexdigest()) self.assertEqual({ release1: { - 'seqno': 5, + 'seqno': 10, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:1')[0]).guid, 'version': [[1], 0], 'requires': {}, - 'command': 'true', + 'commands': {'activity': {'exec': 'true'}}, 'bundles': {'*-*': {'blob': str(hashlib.sha1(bundle1).hexdigest()), 'unpack_size': len(activity_info1)}}, 'stability': 'stable', }, }, release2: { - 'seqno': 7, + 'seqno': 13, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:2')[0]).guid, 'version': [[2], 0], 'requires': {}, - 'command': 'true', + 'commands': {'activity': {'exec': 'true'}}, 'bundles': {'*-*': {'blob': str(hashlib.sha1(bundle2).hexdigest()), 'unpack_size': len(activity_info2)}}, 'stability': 'stable', }, }, }, conn.get(['context', context, 'releases'])) - assert blobs.get(str(hashlib.sha1(bundle1).hexdigest())) - assert blobs.get(str(hashlib.sha1(bundle2).hexdigest())) + assert volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists + assert volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists conn.delete(['context', context, 'releases', release1]) self.assertEqual({ release1: { - 'seqno': 8, + 'seqno': 15, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, }, release2: { - 'seqno': 7, + 'seqno': 13, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:2')[0]).guid, 'version': [[2], 0], 'requires': {}, - 'command': 'true', + 'commands': {'activity': {'exec': 'true'}}, 'bundles': {'*-*': {'blob': str(hashlib.sha1(bundle2).hexdigest()), 'unpack_size': len(activity_info2)}}, 'stability': 'stable', }, }, }, conn.get(['context', context, 'releases'])) - assert blobs.get(str(hashlib.sha1(bundle1).hexdigest())) is None - assert blobs.get(str(hashlib.sha1(bundle2).hexdigest())) + assert not volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists + assert volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists conn.delete(['context', context, 'releases', release2]) self.assertEqual({ release1: { - 'seqno': 8, + 'seqno': 15, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, }, release2: { - 'seqno': 9, + 'seqno': 17, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, }, }, conn.get(['context', context, 'releases'])) - assert blobs.get(str(hashlib.sha1(bundle1).hexdigest())) is None - assert blobs.get(str(hashlib.sha1(bundle2).hexdigest())) is None + assert not volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists + assert not volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists def test_IncrementReleasesSeqnoOnNewReleases(self): events = [] @@ -287,13 +287,29 @@ class ContextTest(tests.Test): ], [i for i in events if i['event'] == 'release']) self.assertEqual(0, volume.releases_seqno.value) + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual([ + {'seqno': 1, 'event': 'release'} + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(1, volume.releases_seqno.value) + del events[:] + conn.put(['context', context], { 'dependencies': 'dep', }) self.assertEqual([ - {'event': 'release', 'seqno': 1}, + {'event': 'release', 'seqno': 2}, ], [i for i in events if i['event'] == 'release']) - self.assertEqual(1, volume.releases_seqno.value) + self.assertEqual(2, volume.releases_seqno.value) def test_IncrementReleasesSeqnoOnDeletes(self): events = [] @@ -311,22 +327,28 @@ class ContextTest(tests.Test): ], [i for i in events if i['event'] == 'release']) self.assertEqual(0, volume.releases_seqno.value) - conn.put(['context', context], { - 'layer': ['deleted'], - }) + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) self.assertEqual([ - {'event': 'release', 'seqno': 1}, + {'seqno': 1, 'event': 'release'} ], [i for i in events if i['event'] == 'release']) self.assertEqual(1, volume.releases_seqno.value) + del events[:] - conn.put(['context', context], { - 'layer': [], - }) + conn.delete(['context', context]) self.assertEqual([ - {'event': 'release', 'seqno': 1}, {'event': 'release', 'seqno': 2}, ], [i for i in events if i['event'] == 'release']) self.assertEqual(2, volume.releases_seqno.value) + del events[:] def test_RestoreReleasesSeqno(self): events = [] @@ -341,6 +363,16 @@ class ContextTest(tests.Test): 'description': 'description', 'dependencies': 'dep', }) + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) self.assertEqual(1, volume.releases_seqno.value) volume.close() diff --git a/tests/units/model/model.py b/tests/units/model/model.py index 49fd1a3..857a54b 100755 --- a/tests/units/model/model.py +++ b/tests/units/model/model.py @@ -10,6 +10,8 @@ from __init__ import tests from sugar_network import db from sugar_network.model import load_bundle from sugar_network.model.post import Post +from sugar_network.model.context import Context +from sugar_network.node.model import User from sugar_network.client import IPCConnection, Connection, keyfile from sugar_network.toolkit.router import Request from sugar_network.toolkit.coroutine import this @@ -19,6 +21,7 @@ from sugar_network.toolkit import i18n, http, coroutine, enforce class ModelTest(tests.Test): def test_RatingSort(self): + this.localcast = lambda event: None directory = db.Volume('db', [Post])['post'] directory.create({'guid': '1', 'context': '', 'type': 'post', 'title': {}, 'message': {}, 'rating': [0, 0]}) @@ -518,7 +521,7 @@ class ModelTest(tests.Test): release['requires']) def test_load_bundle_IgnoreNotSupportedContextTypes(self): - volume = self.start_master() + volume = self.start_master([User, Context]) conn = Connection(auth=http.SugarAuth(keyfile.value)) context = conn.post(['context'], { @@ -528,9 +531,9 @@ class ModelTest(tests.Test): 'description': '', }) this.request = Request(method='POST', path=['context', context]) - aggid = conn.post(['context', context, 'releases'], -1) + aggid = conn.post(['context', context, 'releases'], {}) self.assertEqual({ - aggid: {'seqno': 4, 'value': -1, 'author': {tests.UID: {'role': 3, 'name': tests.UID, 'order': 0}}}, + aggid: {'seqno': 4, 'value': {}, 'author': {tests.UID: {'role': 3, 'name': tests.UID, 'order': 0}}}, }, volume['context'][context]['releases']) diff --git a/tests/units/model/post.py b/tests/units/model/post.py index 45b85e1..655c08e 100755 --- a/tests/units/model/post.py +++ b/tests/units/model/post.py @@ -5,7 +5,6 @@ from __init__ import tests from sugar_network import db from sugar_network.client import Connection, keyfile -from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.model.post import Post from sugar_network.toolkit.coroutine import this diff --git a/tests/units/model/routes.py b/tests/units/model/routes.py index be5ecdf..06a3dc3 100755 --- a/tests/units/model/routes.py +++ b/tests/units/model/routes.py @@ -10,7 +10,6 @@ from os.path import exists from __init__ import tests, src_root from sugar_network import db, model -from sugar_network.model.user import User from sugar_network.toolkit.router import Router, Request from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import coroutine @@ -49,7 +48,7 @@ class RoutesTest(tests.Test): self.assertEqual([ {'event': 'pong'}, {'guid': 'guid', 'resource': 'document', 'event': 'create'}, - {'guid': 'guid', 'resource': 'document', 'event': 'update'}, + {'guid': 'guid', 'resource': 'document', 'event': 'update', 'props': {'prop': 'value2'}}, {'guid': 'guid', 'event': 'delete', 'resource': u'document'}, ], events) diff --git a/tests/units/node/master.py b/tests/units/node/master.py index 69fc6a7..2577ba9 100755 --- a/tests/units/node/master.py +++ b/tests/units/node/master.py @@ -20,8 +20,8 @@ from sugar_network.client import Connection, keyfile, api from sugar_network.db.directory import Directory from sugar_network import db, node, toolkit from sugar_network.node.master import MasterRoutes +from sugar_network.node.model import User from sugar_network.db.volume import Volume -from sugar_network.model.user import User from sugar_network.toolkit.router import Response, File from sugar_network.toolkit import coroutine, parcel, http @@ -64,7 +64,7 @@ class MasterTest(tests.Test): response = conn.request('POST', [], patch, params={'cmd': 'push'}) reply = parcel.decode(response.raw) - assert volume['document'].exists('1') + assert volume['document']['1'].exists blob = volume.blobs.get(hashlib.sha1('1').hexdigest()) self.assertEqual('1', ''.join(blob.iter_content())) blob = volume.blobs.get('foo/bar') @@ -165,7 +165,7 @@ class MasterTest(tests.Test): }) reply = parcel.decode(response.raw) - assert volume['document'].exists('1') + assert volume['document']['1'].exists blob_digest = hashlib.sha1('blob').hexdigest() blob = volume.blobs.get(blob_digest) self.assertEqual('blob', ''.join(blob.iter_content())) @@ -541,7 +541,7 @@ class MasterTest(tests.Test): ], [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) - assert volume['document'].exists('2') + assert volume['document']['2'].exists self.assertEqual('ccc', ''.join(blob2.iter_content())) diff --git a/tests/units/node/model.py b/tests/units/node/model.py index 6788105..4187d3c 100755 --- a/tests/units/node/model.py +++ b/tests/units/node/model.py @@ -8,10 +8,10 @@ from __init__ import tests from sugar_network import db, toolkit from sugar_network.client import Connection, keyfile, api -from sugar_network.model.user import User from sugar_network.model.post import Post from sugar_network.model.context import Context from sugar_network.node import model, obs +from sugar_network.node.model import User from sugar_network.node.routes import NodeRoutes from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import Request, Router diff --git a/tests/units/node/node.py b/tests/units/node/node.py index 0f934d4..89373dc 100755 --- a/tests/units/node/node.py +++ b/tests/units/node/node.py @@ -20,9 +20,8 @@ from sugar_network.client import Connection, keyfile, api from sugar_network.toolkit import http, coroutine from sugar_network.node.routes import NodeRoutes from sugar_network.node.master import MasterRoutes -from sugar_network.model.user import User from sugar_network.model.context import Context -from sugar_network.model.user import User +from sugar_network.node.model import User from sugar_network.toolkit.router import Router, Request, Response, fallbackroute, ACL, route from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http @@ -30,72 +29,6 @@ from sugar_network.toolkit import http class NodeTest(tests.Test): - def test_HandleDeletes(self): - volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) - volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - - guid = this.call(method='POST', path=['context'], principal=tests.UID, content={ - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - guid_path = 'master/db/context/%s/%s' % (guid[:2], guid) - - assert exists(guid_path) - self.assertEqual({ - 'guid': guid, - 'title': 'title', - 'layer': [], - }, - this.call(method='GET', path=['context', guid], reply=['guid', 'title', 'layer'])) - self.assertEqual([], volume['context'].get(guid)['layer']) - - def subscribe(): - for event in conn.subscribe(): - events.append(event) - events = [] - coroutine.spawn(subscribe) - coroutine.dispatch() - - this.call(method='DELETE', path=['context', guid], principal=tests.UID) - coroutine.dispatch() - self.assertRaises(http.NotFound, this.call, method='GET', path=['context', guid], reply=['guid', 'title']) - self.assertEqual(['deleted'], volume['context'].get(guid)['layer']) - - def test_DeletedRestoredHandlers(self): - trigger = [] - - class TestDocument(db.Resource): - - def deleted(self): - trigger.append(False) - - def restored(self): - trigger.append(True) - - volume = self.start_master([TestDocument, User]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) - - guid = conn.post(['testdocument'], {}) - self.assertEqual([], trigger) - - conn.put(['testdocument', guid, 'layer'], ['deleted']) - self.assertEqual([False], trigger) - - conn.put(['testdocument', guid, 'layer'], []) - self.assertEqual([False, True], trigger) - - conn.put(['testdocument', guid, 'layer'], ['bar']) - self.assertEqual([False, True], trigger) - - conn.put(['testdocument', guid, 'layer'], ['deleted']) - self.assertEqual([False, True, False], trigger) - - conn.put(['testdocument', guid, 'layer'], ['deleted', 'foo']) - self.assertEqual([False, True, False], trigger) - def test_RegisterUser(self): volume = self.start_master() conn = Connection(auth=http.SugarAuth(keyfile.value)) @@ -369,7 +302,7 @@ class NodeTest(tests.Test): this.call(method='GET', path=['context', guid]) self.assertNotEqual([], this.call(method='GET', path=['context'])['result']) - volume['context'].update(guid, {'layer': ['deleted']}) + volume['context'].update(guid, {'state': 'deleted'}) self.assertRaises(http.NotFound, this.call, method='GET', path=['context', guid]) self.assertEqual([], this.call(method='GET', path=['context'])['result']) @@ -415,7 +348,7 @@ class NodeTest(tests.Test): 'description': 'description', }) - self.assertRaises(http.BadRequest, this.call, method='POST', path=['context'], principal=tests.UID, content={ + self.assertRaises(RuntimeError, this.call, method='POST', path=['context'], principal=tests.UID, content={ 'guid': guid, 'type': 'activity', 'title': 'title', @@ -496,7 +429,7 @@ class NodeTest(tests.Test): self.assertEqual({ release: { - 'seqno': 6, + 'seqno': 9, 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], @@ -568,6 +501,7 @@ class NodeTest(tests.Test): 'version': [[1], 0], 'size': len(activity_pack), 'unpack_size': len(activity_unpack), + 'content-type': 'application/vnd.olpc-sugar', }, 'dep': { 'title': 'dep', @@ -575,6 +509,7 @@ class NodeTest(tests.Test): 'version': [[2], 0], 'size': len(dep_pack), 'unpack_size': len(dep_unpack), + 'content-type': 'application/vnd.olpc-sugar', }, 'package': { 'packages': ['package.bin'], @@ -646,6 +581,7 @@ class NodeTest(tests.Test): 'version': [[1], 0], 'size': len(activity_pack), 'unpack_size': len(activity_unpack), + 'content-type': 'application/vnd.olpc-sugar', }, 'dep': { 'title': 'dep', @@ -653,6 +589,7 @@ class NodeTest(tests.Test): 'version': [[2], 0], 'size': len(dep_pack), 'unpack_size': len(dep_unpack), + 'content-type': 'application/vnd.olpc-sugar', }, 'package': { 'packages': ['package.bin'], @@ -662,7 +599,7 @@ class NodeTest(tests.Test): conn.get(['context', 'activity'], cmd='solve', stability='developer', lsb_id='Ubuntu', lsb_release='10.04', requires=['dep', 'package'])) - def test_Clone(self): + def test_Resolve(self): volume = self.start_master() conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) @@ -699,7 +636,7 @@ class NodeTest(tests.Test): conn.put(['context', 'package', 'releases', '*'], {'binary': ['package.bin']}) response = Response() - reply = conn.call(Request(method='GET', path=['context', 'activity'], cmd='clone'), response) + reply = conn.call(Request(method='GET', path=['context', 'activity'], cmd='resolve'), response) assert activity_blob == reply.read() def test_AggpropInsertAccess(self): diff --git a/tests/units/node/slave.py b/tests/units/node/slave.py index 55da003..3afab04 100755 --- a/tests/units/node/slave.py +++ b/tests/units/node/slave.py @@ -14,8 +14,8 @@ from sugar_network.client import Connection, keyfile from sugar_network.node import master_api from sugar_network.node.master import MasterRoutes from sugar_network.node.slave import SlaveRoutes +from sugar_network.node.model import User from sugar_network.db.volume import Volume -from sugar_network.model.user import User from sugar_network.toolkit.router import Router, File from sugar_network.toolkit import coroutine, http, parcel diff --git a/tests/units/toolkit/router.py b/tests/units/toolkit/router.py index 0c18cee..61d8dff 100755 --- a/tests/units/toolkit/router.py +++ b/tests/units/toolkit/router.py @@ -550,6 +550,7 @@ class RouterTest(tests.Test): @postroute def _(self, request, response, result, exception): + print exception postroutes.append(('_', result, str(exception))) class B1(A): diff --git a/tests/units/toolkit/toolkit.py b/tests/units/toolkit/toolkit.py index b3e7f6b..87baedc 100755 --- a/tests/units/toolkit/toolkit.py +++ b/tests/units/toolkit/toolkit.py @@ -16,16 +16,12 @@ class ToolkitTest(tests.Test): def test_Seqno_commit(self): seqno = Seqno(tests.tmpdir + '/seqno') - self.assertEqual(False, seqno.commit()) - seqno.next() - self.assertEqual(True, seqno.commit()) - self.assertEqual(False, seqno.commit()) + seqno.commit() seqno.next() seqno = Seqno(tests.tmpdir + '/seqno') self.assertEqual(1, seqno.value) - self.assertEqual(False, seqno.commit()) def test_readline(self): |