Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2014-03-24 11:55:25 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2014-03-24 11:55:25 (GMT)
commit6ec16441c7c133c55385613f1e430c5ea37af632 (patch)
tree51870c8fa43a3bcabc6918206b3fc5265a91300a
parent40021927aa1815dd54e2e7839a46e5bd1ae8c7b3 (diff)
Fix basic client routes
-rw-r--r--TODO1
-rw-r--r--doc/objects.dia77
-rwxr-xr-xsugar-network-client15
-rwxr-xr-xsugar-network-node6
-rwxr-xr-xsugar-network-sync33
-rw-r--r--sugar_network/client/__init__.py7
-rw-r--r--sugar_network/client/injector.py113
-rw-r--r--sugar_network/client/journal.py13
-rw-r--r--sugar_network/client/model.py36
-rw-r--r--sugar_network/client/routes.py293
-rw-r--r--sugar_network/db/directory.py36
-rw-r--r--sugar_network/db/metadata.py3
-rw-r--r--sugar_network/db/resource.py62
-rw-r--r--sugar_network/db/routes.py130
-rw-r--r--sugar_network/db/volume.py24
-rw-r--r--sugar_network/model/__init__.py30
-rw-r--r--sugar_network/model/context.py46
-rw-r--r--sugar_network/model/post.py3
-rw-r--r--sugar_network/model/report.py3
-rw-r--r--sugar_network/model/routes.py1
-rw-r--r--sugar_network/node/master.py10
-rw-r--r--sugar_network/node/model.py22
-rw-r--r--sugar_network/node/routes.py16
-rw-r--r--sugar_network/node/slave.py23
-rw-r--r--sugar_network/toolkit/coroutine.py5
-rw-r--r--sugar_network/toolkit/http.py3
-rw-r--r--sugar_network/toolkit/router.py36
-rw-r--r--tests/__init__.py72
-rw-r--r--tests/units/client/__main__.py3
-rwxr-xr-xtests/units/client/injector.py12
-rwxr-xr-xtests/units/client/journal.py10
-rwxr-xr-xtests/units/client/offline_routes.py560
-rwxr-xr-xtests/units/client/online_routes.py1611
-rwxr-xr-xtests/units/client/routes.py1242
-rwxr-xr-xtests/units/client/server_routes.py226
-rwxr-xr-xtests/units/db/resource.py35
-rwxr-xr-xtests/units/db/routes.py93
-rwxr-xr-xtests/units/db/volume.py51
-rwxr-xr-xtests/units/model/context.py88
-rwxr-xr-xtests/units/model/model.py9
-rwxr-xr-xtests/units/model/post.py1
-rwxr-xr-xtests/units/model/routes.py3
-rwxr-xr-xtests/units/node/master.py8
-rwxr-xr-xtests/units/node/model.py2
-rwxr-xr-xtests/units/node/node.py83
-rwxr-xr-xtests/units/node/slave.py2
-rwxr-xr-xtests/units/toolkit/router.py1
-rwxr-xr-xtests/units/toolkit/toolkit.py6
48 files changed, 1849 insertions, 3316 deletions
diff --git a/TODO b/TODO
index dc40f32..5244533 100644
--- a/TODO
+++ b/TODO
@@ -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):