##// END OF EJS Templates
sshpeer: establish SSH connection before class instantiation...
Gregory Szorc -
r35953:00b9e26d default
parent child Browse files
Show More
@@ -1,385 +1,391 b''
1 # sshpeer.py - ssh repository proxy class for mercurial
1 # sshpeer.py - ssh repository proxy class for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 wireproto,
17 wireproto,
18 )
18 )
19
19
20 def _serverquote(s):
20 def _serverquote(s):
21 """quote a string for the remote shell ... which we assume is sh"""
21 """quote a string for the remote shell ... which we assume is sh"""
22 if not s:
22 if not s:
23 return s
23 return s
24 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
24 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
25 return s
25 return s
26 return "'%s'" % s.replace("'", "'\\''")
26 return "'%s'" % s.replace("'", "'\\''")
27
27
28 def _forwardoutput(ui, pipe):
28 def _forwardoutput(ui, pipe):
29 """display all data currently available on pipe as remote output.
29 """display all data currently available on pipe as remote output.
30
30
31 This is non blocking."""
31 This is non blocking."""
32 s = util.readpipe(pipe)
32 s = util.readpipe(pipe)
33 if s:
33 if s:
34 for l in s.splitlines():
34 for l in s.splitlines():
35 ui.status(_("remote: "), l, '\n')
35 ui.status(_("remote: "), l, '\n')
36
36
37 class doublepipe(object):
37 class doublepipe(object):
38 """Operate a side-channel pipe in addition of a main one
38 """Operate a side-channel pipe in addition of a main one
39
39
40 The side-channel pipe contains server output to be forwarded to the user
40 The side-channel pipe contains server output to be forwarded to the user
41 input. The double pipe will behave as the "main" pipe, but will ensure the
41 input. The double pipe will behave as the "main" pipe, but will ensure the
42 content of the "side" pipe is properly processed while we wait for blocking
42 content of the "side" pipe is properly processed while we wait for blocking
43 call on the "main" pipe.
43 call on the "main" pipe.
44
44
45 If large amounts of data are read from "main", the forward will cease after
45 If large amounts of data are read from "main", the forward will cease after
46 the first bytes start to appear. This simplifies the implementation
46 the first bytes start to appear. This simplifies the implementation
47 without affecting actual output of sshpeer too much as we rarely issue
47 without affecting actual output of sshpeer too much as we rarely issue
48 large read for data not yet emitted by the server.
48 large read for data not yet emitted by the server.
49
49
50 The main pipe is expected to be a 'bufferedinputpipe' from the util module
50 The main pipe is expected to be a 'bufferedinputpipe' from the util module
51 that handle all the os specific bits. This class lives in this module
51 that handle all the os specific bits. This class lives in this module
52 because it focus on behavior specific to the ssh protocol."""
52 because it focus on behavior specific to the ssh protocol."""
53
53
54 def __init__(self, ui, main, side):
54 def __init__(self, ui, main, side):
55 self._ui = ui
55 self._ui = ui
56 self._main = main
56 self._main = main
57 self._side = side
57 self._side = side
58
58
59 def _wait(self):
59 def _wait(self):
60 """wait until some data are available on main or side
60 """wait until some data are available on main or side
61
61
62 return a pair of boolean (ismainready, issideready)
62 return a pair of boolean (ismainready, issideready)
63
63
64 (This will only wait for data if the setup is supported by `util.poll`)
64 (This will only wait for data if the setup is supported by `util.poll`)
65 """
65 """
66 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
66 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
67 return (True, True) # main has data, assume side is worth poking at.
67 return (True, True) # main has data, assume side is worth poking at.
68 fds = [self._main.fileno(), self._side.fileno()]
68 fds = [self._main.fileno(), self._side.fileno()]
69 try:
69 try:
70 act = util.poll(fds)
70 act = util.poll(fds)
71 except NotImplementedError:
71 except NotImplementedError:
72 # non supported yet case, assume all have data.
72 # non supported yet case, assume all have data.
73 act = fds
73 act = fds
74 return (self._main.fileno() in act, self._side.fileno() in act)
74 return (self._main.fileno() in act, self._side.fileno() in act)
75
75
76 def write(self, data):
76 def write(self, data):
77 return self._call('write', data)
77 return self._call('write', data)
78
78
79 def read(self, size):
79 def read(self, size):
80 r = self._call('read', size)
80 r = self._call('read', size)
81 if size != 0 and not r:
81 if size != 0 and not r:
82 # We've observed a condition that indicates the
82 # We've observed a condition that indicates the
83 # stdout closed unexpectedly. Check stderr one
83 # stdout closed unexpectedly. Check stderr one
84 # more time and snag anything that's there before
84 # more time and snag anything that's there before
85 # letting anyone know the main part of the pipe
85 # letting anyone know the main part of the pipe
86 # closed prematurely.
86 # closed prematurely.
87 _forwardoutput(self._ui, self._side)
87 _forwardoutput(self._ui, self._side)
88 return r
88 return r
89
89
90 def readline(self):
90 def readline(self):
91 return self._call('readline')
91 return self._call('readline')
92
92
93 def _call(self, methname, data=None):
93 def _call(self, methname, data=None):
94 """call <methname> on "main", forward output of "side" while blocking
94 """call <methname> on "main", forward output of "side" while blocking
95 """
95 """
96 # data can be '' or 0
96 # data can be '' or 0
97 if (data is not None and not data) or self._main.closed:
97 if (data is not None and not data) or self._main.closed:
98 _forwardoutput(self._ui, self._side)
98 _forwardoutput(self._ui, self._side)
99 return ''
99 return ''
100 while True:
100 while True:
101 mainready, sideready = self._wait()
101 mainready, sideready = self._wait()
102 if sideready:
102 if sideready:
103 _forwardoutput(self._ui, self._side)
103 _forwardoutput(self._ui, self._side)
104 if mainready:
104 if mainready:
105 meth = getattr(self._main, methname)
105 meth = getattr(self._main, methname)
106 if data is None:
106 if data is None:
107 return meth()
107 return meth()
108 else:
108 else:
109 return meth(data)
109 return meth(data)
110
110
111 def close(self):
111 def close(self):
112 return self._main.close()
112 return self._main.close()
113
113
114 def flush(self):
114 def flush(self):
115 return self._main.flush()
115 return self._main.flush()
116
116
117 def _cleanuppipes(ui, pipei, pipeo, pipee):
117 def _cleanuppipes(ui, pipei, pipeo, pipee):
118 """Clean up pipes used by an SSH connection."""
118 """Clean up pipes used by an SSH connection."""
119 if pipeo:
119 if pipeo:
120 pipeo.close()
120 pipeo.close()
121 if pipei:
121 if pipei:
122 pipei.close()
122 pipei.close()
123
123
124 if pipee:
124 if pipee:
125 # Try to read from the err descriptor until EOF.
125 # Try to read from the err descriptor until EOF.
126 try:
126 try:
127 for l in pipee:
127 for l in pipee:
128 ui.status(_('remote: '), l)
128 ui.status(_('remote: '), l)
129 except (IOError, ValueError):
129 except (IOError, ValueError):
130 pass
130 pass
131
131
132 pipee.close()
132 pipee.close()
133
133
134 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
135 """Create an SSH connection to a server.
136
137 Returns a tuple of (process, stdin, stdout, stderr) for the
138 spawned process.
139 """
140 cmd = '%s %s %s' % (
141 sshcmd,
142 args,
143 util.shellquote('%s -R %s serve --stdio' % (
144 _serverquote(remotecmd), _serverquote(path))))
145
146 ui.debug('running %s\n' % cmd)
147 cmd = util.quotecommand(cmd)
148
149 # no buffer allow the use of 'select'
150 # feel free to remove buffering and select usage when we ultimately
151 # move to threading.
152 stdin, stdout, stderr, proc = util.popen4(cmd, bufsize=0, env=sshenv)
153
154 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
155 stdin = doublepipe(ui, stdin, stderr)
156
157 return proc, stdin, stdout, stderr
158
134 class sshpeer(wireproto.wirepeer):
159 class sshpeer(wireproto.wirepeer):
135 def __init__(self, ui, path, create=False, sshstate=None):
160 def __init__(self, ui, path, create=False, sshstate=None):
136 self._url = path
161 self._url = path
137 self._ui = ui
162 self._ui = ui
138 self._pipeo = self._pipei = self._pipee = None
163 # self._subprocess is unused. Keeping a handle on the process
164 # holds a reference and prevents it from being garbage collected.
165 self._subprocess, self._pipei, self._pipeo, self._pipee = sshstate
139
166
140 u = util.url(path, parsequery=False, parsefragment=False)
167 self._validaterepo()
141 self._path = u.path or '.'
142
143 self._validaterepo(*sshstate)
144
168
145 # Begin of _basepeer interface.
169 # Begin of _basepeer interface.
146
170
147 @util.propertycache
171 @util.propertycache
148 def ui(self):
172 def ui(self):
149 return self._ui
173 return self._ui
150
174
151 def url(self):
175 def url(self):
152 return self._url
176 return self._url
153
177
154 def local(self):
178 def local(self):
155 return None
179 return None
156
180
157 def peer(self):
181 def peer(self):
158 return self
182 return self
159
183
160 def canpush(self):
184 def canpush(self):
161 return True
185 return True
162
186
163 def close(self):
187 def close(self):
164 pass
188 pass
165
189
166 # End of _basepeer interface.
190 # End of _basepeer interface.
167
191
168 # Begin of _basewirecommands interface.
192 # Begin of _basewirecommands interface.
169
193
170 def capabilities(self):
194 def capabilities(self):
171 return self._caps
195 return self._caps
172
196
173 # End of _basewirecommands interface.
197 # End of _basewirecommands interface.
174
198
175 def _validaterepo(self, sshcmd, args, remotecmd, sshenv=None):
199 def _validaterepo(self):
176 assert self._pipei is None
177
178 cmd = '%s %s %s' % (sshcmd, args,
179 util.shellquote("%s -R %s serve --stdio" %
180 (_serverquote(remotecmd), _serverquote(self._path))))
181 self.ui.debug('running %s\n' % cmd)
182 cmd = util.quotecommand(cmd)
183
184 # while self._subprocess isn't used, having it allows the subprocess to
185 # to clean up correctly later
186 #
187 # no buffer allow the use of 'select'
188 # feel free to remove buffering and select usage when we ultimately
189 # move to threading.
190 sub = util.popen4(cmd, bufsize=0, env=sshenv)
191 self._pipeo, self._pipei, self._pipee, self._subprocess = sub
192
193 self._pipei = util.bufferedinputpipe(self._pipei)
194 self._pipei = doublepipe(self.ui, self._pipei, self._pipee)
195 self._pipeo = doublepipe(self.ui, self._pipeo, self._pipee)
196
197 def badresponse():
200 def badresponse():
198 msg = _("no suitable response from remote hg")
201 msg = _("no suitable response from remote hg")
199 hint = self.ui.config("ui", "ssherrorhint")
202 hint = self.ui.config("ui", "ssherrorhint")
200 self._abort(error.RepoError(msg, hint=hint))
203 self._abort(error.RepoError(msg, hint=hint))
201
204
202 try:
205 try:
203 # skip any noise generated by remote shell
206 # skip any noise generated by remote shell
204 self._callstream("hello")
207 self._callstream("hello")
205 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
208 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
206 except IOError:
209 except IOError:
207 badresponse()
210 badresponse()
208
211
209 lines = ["", "dummy"]
212 lines = ["", "dummy"]
210 max_noise = 500
213 max_noise = 500
211 while lines[-1] and max_noise:
214 while lines[-1] and max_noise:
212 try:
215 try:
213 l = r.readline()
216 l = r.readline()
214 self._readerr()
217 self._readerr()
215 if lines[-1] == "1\n" and l == "\n":
218 if lines[-1] == "1\n" and l == "\n":
216 break
219 break
217 if l:
220 if l:
218 self.ui.debug("remote: ", l)
221 self.ui.debug("remote: ", l)
219 lines.append(l)
222 lines.append(l)
220 max_noise -= 1
223 max_noise -= 1
221 except IOError:
224 except IOError:
222 badresponse()
225 badresponse()
223 else:
226 else:
224 badresponse()
227 badresponse()
225
228
226 self._caps = set()
229 self._caps = set()
227 for l in reversed(lines):
230 for l in reversed(lines):
228 if l.startswith("capabilities:"):
231 if l.startswith("capabilities:"):
229 self._caps.update(l[:-1].split(":")[1].split())
232 self._caps.update(l[:-1].split(":")[1].split())
230 break
233 break
231
234
232 def _readerr(self):
235 def _readerr(self):
233 _forwardoutput(self.ui, self._pipee)
236 _forwardoutput(self.ui, self._pipee)
234
237
235 def _abort(self, exception):
238 def _abort(self, exception):
236 self._cleanup()
239 self._cleanup()
237 raise exception
240 raise exception
238
241
239 def _cleanup(self):
242 def _cleanup(self):
240 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
243 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
241
244
242 __del__ = _cleanup
245 __del__ = _cleanup
243
246
244 def _submitbatch(self, req):
247 def _submitbatch(self, req):
245 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
248 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
246 available = self._getamount()
249 available = self._getamount()
247 # TODO this response parsing is probably suboptimal for large
250 # TODO this response parsing is probably suboptimal for large
248 # batches with large responses.
251 # batches with large responses.
249 toread = min(available, 1024)
252 toread = min(available, 1024)
250 work = rsp.read(toread)
253 work = rsp.read(toread)
251 available -= toread
254 available -= toread
252 chunk = work
255 chunk = work
253 while chunk:
256 while chunk:
254 while ';' in work:
257 while ';' in work:
255 one, work = work.split(';', 1)
258 one, work = work.split(';', 1)
256 yield wireproto.unescapearg(one)
259 yield wireproto.unescapearg(one)
257 toread = min(available, 1024)
260 toread = min(available, 1024)
258 chunk = rsp.read(toread)
261 chunk = rsp.read(toread)
259 available -= toread
262 available -= toread
260 work += chunk
263 work += chunk
261 yield wireproto.unescapearg(work)
264 yield wireproto.unescapearg(work)
262
265
263 def _callstream(self, cmd, **args):
266 def _callstream(self, cmd, **args):
264 args = pycompat.byteskwargs(args)
267 args = pycompat.byteskwargs(args)
265 if (self.ui.debugflag
268 if (self.ui.debugflag
266 and self.ui.configbool('devel', 'debug.peer-request')):
269 and self.ui.configbool('devel', 'debug.peer-request')):
267 dbg = self.ui.debug
270 dbg = self.ui.debug
268 line = 'devel-peer-request: %s\n'
271 line = 'devel-peer-request: %s\n'
269 dbg(line % cmd)
272 dbg(line % cmd)
270 for key, value in sorted(args.items()):
273 for key, value in sorted(args.items()):
271 if not isinstance(value, dict):
274 if not isinstance(value, dict):
272 dbg(line % ' %s: %d bytes' % (key, len(value)))
275 dbg(line % ' %s: %d bytes' % (key, len(value)))
273 else:
276 else:
274 for dk, dv in sorted(value.items()):
277 for dk, dv in sorted(value.items()):
275 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
278 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
276 self.ui.debug("sending %s command\n" % cmd)
279 self.ui.debug("sending %s command\n" % cmd)
277 self._pipeo.write("%s\n" % cmd)
280 self._pipeo.write("%s\n" % cmd)
278 _func, names = wireproto.commands[cmd]
281 _func, names = wireproto.commands[cmd]
279 keys = names.split()
282 keys = names.split()
280 wireargs = {}
283 wireargs = {}
281 for k in keys:
284 for k in keys:
282 if k == '*':
285 if k == '*':
283 wireargs['*'] = args
286 wireargs['*'] = args
284 break
287 break
285 else:
288 else:
286 wireargs[k] = args[k]
289 wireargs[k] = args[k]
287 del args[k]
290 del args[k]
288 for k, v in sorted(wireargs.iteritems()):
291 for k, v in sorted(wireargs.iteritems()):
289 self._pipeo.write("%s %d\n" % (k, len(v)))
292 self._pipeo.write("%s %d\n" % (k, len(v)))
290 if isinstance(v, dict):
293 if isinstance(v, dict):
291 for dk, dv in v.iteritems():
294 for dk, dv in v.iteritems():
292 self._pipeo.write("%s %d\n" % (dk, len(dv)))
295 self._pipeo.write("%s %d\n" % (dk, len(dv)))
293 self._pipeo.write(dv)
296 self._pipeo.write(dv)
294 else:
297 else:
295 self._pipeo.write(v)
298 self._pipeo.write(v)
296 self._pipeo.flush()
299 self._pipeo.flush()
297
300
298 return self._pipei
301 return self._pipei
299
302
300 def _callcompressable(self, cmd, **args):
303 def _callcompressable(self, cmd, **args):
301 return self._callstream(cmd, **args)
304 return self._callstream(cmd, **args)
302
305
303 def _call(self, cmd, **args):
306 def _call(self, cmd, **args):
304 self._callstream(cmd, **args)
307 self._callstream(cmd, **args)
305 return self._recv()
308 return self._recv()
306
309
307 def _callpush(self, cmd, fp, **args):
310 def _callpush(self, cmd, fp, **args):
308 r = self._call(cmd, **args)
311 r = self._call(cmd, **args)
309 if r:
312 if r:
310 return '', r
313 return '', r
311 for d in iter(lambda: fp.read(4096), ''):
314 for d in iter(lambda: fp.read(4096), ''):
312 self._send(d)
315 self._send(d)
313 self._send("", flush=True)
316 self._send("", flush=True)
314 r = self._recv()
317 r = self._recv()
315 if r:
318 if r:
316 return '', r
319 return '', r
317 return self._recv(), ''
320 return self._recv(), ''
318
321
319 def _calltwowaystream(self, cmd, fp, **args):
322 def _calltwowaystream(self, cmd, fp, **args):
320 r = self._call(cmd, **args)
323 r = self._call(cmd, **args)
321 if r:
324 if r:
322 # XXX needs to be made better
325 # XXX needs to be made better
323 raise error.Abort(_('unexpected remote reply: %s') % r)
326 raise error.Abort(_('unexpected remote reply: %s') % r)
324 for d in iter(lambda: fp.read(4096), ''):
327 for d in iter(lambda: fp.read(4096), ''):
325 self._send(d)
328 self._send(d)
326 self._send("", flush=True)
329 self._send("", flush=True)
327 return self._pipei
330 return self._pipei
328
331
329 def _getamount(self):
332 def _getamount(self):
330 l = self._pipei.readline()
333 l = self._pipei.readline()
331 if l == '\n':
334 if l == '\n':
332 self._readerr()
335 self._readerr()
333 msg = _('check previous remote output')
336 msg = _('check previous remote output')
334 self._abort(error.OutOfBandError(hint=msg))
337 self._abort(error.OutOfBandError(hint=msg))
335 self._readerr()
338 self._readerr()
336 try:
339 try:
337 return int(l)
340 return int(l)
338 except ValueError:
341 except ValueError:
339 self._abort(error.ResponseError(_("unexpected response:"), l))
342 self._abort(error.ResponseError(_("unexpected response:"), l))
340
343
341 def _recv(self):
344 def _recv(self):
342 return self._pipei.read(self._getamount())
345 return self._pipei.read(self._getamount())
343
346
344 def _send(self, data, flush=False):
347 def _send(self, data, flush=False):
345 self._pipeo.write("%d\n" % len(data))
348 self._pipeo.write("%d\n" % len(data))
346 if data:
349 if data:
347 self._pipeo.write(data)
350 self._pipeo.write(data)
348 if flush:
351 if flush:
349 self._pipeo.flush()
352 self._pipeo.flush()
350 self._readerr()
353 self._readerr()
351
354
352 def instance(ui, path, create):
355 def instance(ui, path, create):
353 """Create an SSH peer.
356 """Create an SSH peer.
354
357
355 The returned object conforms to the ``wireproto.wirepeer`` interface.
358 The returned object conforms to the ``wireproto.wirepeer`` interface.
356 """
359 """
357 u = util.url(path, parsequery=False, parsefragment=False)
360 u = util.url(path, parsequery=False, parsefragment=False)
358 if u.scheme != 'ssh' or not u.host or u.path is None:
361 if u.scheme != 'ssh' or not u.host or u.path is None:
359 raise error.RepoError(_("couldn't parse location %s") % path)
362 raise error.RepoError(_("couldn't parse location %s") % path)
360
363
361 util.checksafessh(path)
364 util.checksafessh(path)
362
365
363 if u.passwd is not None:
366 if u.passwd is not None:
364 raise error.RepoError(_('password in URL not supported'))
367 raise error.RepoError(_('password in URL not supported'))
365
368
366 sshcmd = ui.config('ui', 'ssh')
369 sshcmd = ui.config('ui', 'ssh')
367 remotecmd = ui.config('ui', 'remotecmd')
370 remotecmd = ui.config('ui', 'remotecmd')
368 sshaddenv = dict(ui.configitems('sshenv'))
371 sshaddenv = dict(ui.configitems('sshenv'))
369 sshenv = util.shellenviron(sshaddenv)
372 sshenv = util.shellenviron(sshaddenv)
370 remotepath = u.path or '.'
373 remotepath = u.path or '.'
371
374
372 args = util.sshargs(sshcmd, u.host, u.user, u.port)
375 args = util.sshargs(sshcmd, u.host, u.user, u.port)
373
376
374 if create:
377 if create:
375 cmd = '%s %s %s' % (sshcmd, args,
378 cmd = '%s %s %s' % (sshcmd, args,
376 util.shellquote('%s init %s' %
379 util.shellquote('%s init %s' %
377 (_serverquote(remotecmd), _serverquote(remotepath))))
380 (_serverquote(remotecmd), _serverquote(remotepath))))
378 ui.debug('running %s\n' % cmd)
381 ui.debug('running %s\n' % cmd)
379 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
382 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
380 if res != 0:
383 if res != 0:
381 raise error.RepoError(_('could not create remote repo'))
384 raise error.RepoError(_('could not create remote repo'))
382
385
383 sshstate = (sshcmd, args, remotecmd, sshenv)
386 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
387 remotepath, sshenv)
388
389 sshstate = (proc, stdout, stdin, stderr)
384
390
385 return sshpeer(ui, path, create=create, sshstate=sshstate)
391 return sshpeer(ui, path, create=create, sshstate=sshstate)
@@ -1,77 +1,78 b''
1 # Test that certain objects conform to well-defined interfaces.
1 # Test that certain objects conform to well-defined interfaces.
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 from mercurial import (
5 from mercurial import (
6 bundlerepo,
6 bundlerepo,
7 httppeer,
7 httppeer,
8 localrepo,
8 localrepo,
9 sshpeer,
9 sshpeer,
10 statichttprepo,
10 statichttprepo,
11 ui as uimod,
11 ui as uimod,
12 unionrepo,
12 unionrepo,
13 )
13 )
14
14
15 def checkobject(o):
15 def checkobject(o):
16 """Verify a constructed object conforms to interface rules.
16 """Verify a constructed object conforms to interface rules.
17
17
18 An object must have __abstractmethods__ defined.
18 An object must have __abstractmethods__ defined.
19
19
20 All "public" attributes of the object (attributes not prefixed with
20 All "public" attributes of the object (attributes not prefixed with
21 an underscore) must be in __abstractmethods__ or appear on a base class
21 an underscore) must be in __abstractmethods__ or appear on a base class
22 with __abstractmethods__.
22 with __abstractmethods__.
23 """
23 """
24 name = o.__class__.__name__
24 name = o.__class__.__name__
25
25
26 allowed = set()
26 allowed = set()
27 for cls in o.__class__.__mro__:
27 for cls in o.__class__.__mro__:
28 if not getattr(cls, '__abstractmethods__', set()):
28 if not getattr(cls, '__abstractmethods__', set()):
29 continue
29 continue
30
30
31 allowed |= cls.__abstractmethods__
31 allowed |= cls.__abstractmethods__
32 allowed |= {a for a in dir(cls) if not a.startswith('_')}
32 allowed |= {a for a in dir(cls) if not a.startswith('_')}
33
33
34 if not allowed:
34 if not allowed:
35 print('%s does not have abstract methods' % name)
35 print('%s does not have abstract methods' % name)
36 return
36 return
37
37
38 public = {a for a in dir(o) if not a.startswith('_')}
38 public = {a for a in dir(o) if not a.startswith('_')}
39
39
40 for attr in sorted(public - allowed):
40 for attr in sorted(public - allowed):
41 print('public attributes not in abstract interface: %s.%s' % (
41 print('public attributes not in abstract interface: %s.%s' % (
42 name, attr))
42 name, attr))
43
43
44 # Facilitates testing localpeer.
44 # Facilitates testing localpeer.
45 class dummyrepo(object):
45 class dummyrepo(object):
46 def __init__(self):
46 def __init__(self):
47 self.ui = uimod.ui()
47 self.ui = uimod.ui()
48 def filtered(self, name):
48 def filtered(self, name):
49 pass
49 pass
50 def _restrictcapabilities(self, caps):
50 def _restrictcapabilities(self, caps):
51 pass
51 pass
52
52
53 # Facilitates testing sshpeer without requiring an SSH server.
53 # Facilitates testing sshpeer without requiring an SSH server.
54 class testingsshpeer(sshpeer.sshpeer):
54 class testingsshpeer(sshpeer.sshpeer):
55 def _validaterepo(self, *args, **kwargs):
55 def _validaterepo(self, *args, **kwargs):
56 pass
56 pass
57
57
58 class badpeer(httppeer.httppeer):
58 class badpeer(httppeer.httppeer):
59 def __init__(self):
59 def __init__(self):
60 super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
60 super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
61 self.badattribute = True
61 self.badattribute = True
62
62
63 def badmethod(self):
63 def badmethod(self):
64 pass
64 pass
65
65
66 def main():
66 def main():
67 ui = uimod.ui()
67 ui = uimod.ui()
68
68
69 checkobject(badpeer())
69 checkobject(badpeer())
70 checkobject(httppeer.httppeer(ui, 'http://localhost'))
70 checkobject(httppeer.httppeer(ui, 'http://localhost'))
71 checkobject(localrepo.localpeer(dummyrepo()))
71 checkobject(localrepo.localpeer(dummyrepo()))
72 checkobject(testingsshpeer(ui, 'ssh://localhost/foo', False, ()))
72 checkobject(testingsshpeer(ui, 'ssh://localhost/foo', False,
73 (None, None, None, None)))
73 checkobject(bundlerepo.bundlepeer(dummyrepo()))
74 checkobject(bundlerepo.bundlepeer(dummyrepo()))
74 checkobject(statichttprepo.statichttppeer(dummyrepo()))
75 checkobject(statichttprepo.statichttppeer(dummyrepo()))
75 checkobject(unionrepo.unionpeer(dummyrepo()))
76 checkobject(unionrepo.unionpeer(dummyrepo()))
76
77
77 main()
78 main()
General Comments 0
You need to be logged in to leave comments. Login now