# test-batching.py - tests for transparent command batching # # Copyright 2011 Peter Arrenbrecht # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import absolute_import, print_function from edenscm.mercurial import error, peer, util, wireproto from hghave import require # equivalent of repo.repository class thing(object): def hello(self): return "Ready." # equivalent of localrepo.localrepository class localthing(thing): def foo(self, one, two=None): if one: return "%s and %s" % (one, two) return "Nope" def bar(self, b, a): return "%s und %s" % (b, a) def greet(self, name=None): return "Hello, %s" % name def batchiter(self): """Support for local batching.""" return peer.localiterbatcher(self) # usage of "thing" interface def use(it): # Direct call to base method shared between client and server. print(it.hello()) # Direct calls to proxied methods. They cause individual roundtrips. print(it.foo("Un", two="Deux")) print(it.bar("Eins", "Zwei")) # Batched call to a couple of proxied methods. batch = it.batchiter() # The calls return futures to eventually hold results. foo = batch.foo(one="One", two="Two") bar = batch.bar("Eins", "Zwei") bar2 = batch.bar(b="Uno", a="Due") # Future shouldn't be set until we submit(). assert isinstance(foo, peer.future) assert not util.safehasattr(foo, "value") assert not util.safehasattr(bar, "value") batch.submit() # Call results() to obtain results as a generator. results = batch.results() # Future results shouldn't be set until we consume a value. assert not util.safehasattr(foo, "value") foovalue = next(results) assert util.safehasattr(foo, "value") assert foovalue == foo.value print(foo.value) next(results) print(bar.value) next(results) print(bar2.value) # We should be at the end of the results generator. try: next(results) except StopIteration: print("proper end of results generator") else: print("extra emitted element!") # Attempting to call a non-batchable method inside a batch fails. batch = it.batchiter() try: batch.greet(name="John Smith") except error.ProgrammingError as e: print(e) # Attempting to call a local method inside a batch fails. batch = it.batchiter() try: batch.hello() except error.ProgrammingError as e: print(e) # local usage mylocal = localthing() print() print("== Local") use(mylocal) # demo remoting; mimicks what wireproto and HTTP/SSH do # shared def escapearg(plain): return ( plain.replace(":", "::") .replace(",", ":,") .replace(";", ":;") .replace("=", ":=") ) def unescapearg(escaped): return ( escaped.replace(":=", "=") .replace(":;", ";") .replace(":,", ",") .replace("::", ":") ) # server side # equivalent of wireproto's global functions class server(object): def __init__(self, local): self.local = local def _call(self, name, args): args = dict(arg.split("=", 1) for arg in args) return getattr(self, name)(**args) def perform(self, req): print("REQ:", req) name, args = req.split("?", 1) args = args.split("&") vals = dict(arg.split("=", 1) for arg in args) res = getattr(self, name)(**vals) print(" ->", res) return res def batch(self, cmds): res = [] for pair in cmds.split(";"): name, args = pair.split(":", 1) vals = {} for a in args.split(","): if a: n, v = a.split("=") vals[n] = unescapearg(v) res.append(escapearg(getattr(self, name)(**vals))) return ";".join(res) def foo(self, one, two): return mangle(self.local.foo(unmangle(one), unmangle(two))) def bar(self, b, a): return mangle(self.local.bar(unmangle(b), unmangle(a))) def greet(self, name): return mangle(self.local.greet(unmangle(name))) myserver = server(mylocal) # local side # equivalent of wireproto.encode/decodelist, that is, type-specific marshalling # here we just transform the strings a bit to check we're properly en-/decoding def mangle(s): return "".join(chr(ord(c) + 1) for c in s) def unmangle(s): return "".join(chr(ord(c) - 1) for c in s) # equivalent of wireproto.wirerepository and something like http's wire format class remotething(thing): def __init__(self, server): self.server = server def _submitone(self, name, args): req = name + "?" + "&".join(["%s=%s" % (n, v) for n, v in args]) return self.server.perform(req) def _submitbatch(self, cmds): req = [] for name, args in cmds: args = ",".join(n + "=" + escapearg(v) for n, v in args) req.append(name + ":" + args) req = ";".join(req) res = self._submitone("batch", [("cmds", req)]) for r in res.split(";"): yield r def batchiter(self): return wireproto.remoteiterbatcher(self) @peer.batchable def foo(self, one, two=None): encargs = [("one", mangle(one)), ("two", mangle(two))] encresref = peer.future() yield encargs, encresref yield unmangle(encresref.value) @peer.batchable def bar(self, b, a): encresref = peer.future() yield [("b", mangle(b)), ("a", mangle(a))], encresref yield unmangle(encresref.value) # greet is coded directly. It therefore does not support batching. If it # does appear in a batch, the batch is split around greet, and the call to # greet is done in its own roundtrip. def greet(self, name=None): return unmangle(self._submitone("greet", [("name", mangle(name))])) # demo remote usage myproxy = remotething(myserver) print() print("== Remote") use(myproxy)