Merge lp:~jameinel/u1db/u1db-client-commandline into lp:u1db

Proposed by John A Meinel
Status: Merged
Merged at revision: 100
Proposed branch: lp:~jameinel/u1db/u1db-client-commandline
Merge into: lp:u1db
Prerequisite: lp:~jameinel/u1db/commandline
Diff against target: 508 lines (+432/-30)
6 files modified
u1db-client (+22/-0)
u1db/commandline/__init__.py (+14/-0)
u1db/commandline/client.py (+124/-0)
u1db/tests/commandline/__init__.py (+31/-0)
u1db/tests/commandline/test_client.py (+240/-0)
u1db/tests/commandline/test_serve.py (+1/-30)
To merge this branch: bzr merge lp:~jameinel/u1db/u1db-client-commandline
Reviewer Review Type Date Requested Status
Samuele Pedroni Approve
Review via email: mp+80855@code.launchpad.net

Description of the change

This implements a 'u1db-client' commandline utility.

This allows us to do 'u1db-client get/put/create/sync' from the commandline.

I've hopefully written it in such a way that we can fairly easily test behaviour without having to run a full subprocess and parsing arguments, etc. So for each command we have:

1) setup_arg_parser() is updated to create a sub command, along with the args that the subcommand needs.
2) a 'client_*' function that gets passed an 'args' structure, this unpacks that structure into
3) a 'cmd_*' function that takes the arguments as named arguments, along with stuff like a file to write to, or a file to read from.

There is a fair amount of boilerplate for each command, but I don't think it is terribly onerous. If we were going to have 100s of commands I might think more about making it more automated.

This doesn't yet implement remote synchronization, because Samuele's patches haven't landed yet. When it does, we basically need to update cmd_sync so that it notices the target is a remote location and uses RemoteSyncTarget.

Some particular concerns:

1) 'get' writes the document content to stdout (or a named file), and the meta-information to stderr. 'put' reads the document content from stdin and the meta-information from the command-line, 'create' reads the meta-information from the command-line, but because 'doc-id' is optional it takes it as a '--doc-id=' instead of a positional argument, it also writes the meta-information to stdout.

Do we want meta-information to always be written to stderr? I like the idea of putting it into a separate channel than where the document content goes.

2) sync doesn't generate any output. We should look into our apis to try to find ways so we can do stuff like progress indication. We may want to think about putting stuff like 'number of documents' in the stream, so you can give the user X/YYY progress indication.

To post a comment you must log in.
Revision history for this message
Samuele Pedroni (pedronis) wrote :

- yes, I think that if we want to segregate metadata to stderr we should do so consistently,
  if we don't then we need anyway I way for people to specify a separate metadata output file, and have a way to ask for stderr there

- I'm not sure whether about the doc_ prefixes in the metadata output and arguments, it's good because it matches the api naming on the other hand it feels a bit overkill

- I'm of the school that would have already tried to use classes for each command and tried to avoid some of the boilerplate, mostly I worry that setup_arg_parser is borderline in size

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/02/2011 03:11 PM, Samuele Pedroni wrote:
> - yes, I think that if we want to segregate metadata to stderr we
> should do so consistently, if we don't then we need anyway I way
> for people to specify a separate metadata output file, and have a
> way to ask for stderr there
>

I think the idea was that 'put' doesn't have any other output, so
'stdout' was left available to write whatever.

Also, 'cmd_get' takes an optional file to write to (maybe that should
be specified with '-o' instead of a positional argument). So we
*could* use that as the way for people to distinguish between actual
content and metadata. For now, I'm happy to just use stderr everywhere
if that seems preferable.

> - I'm not sure whether about the doc_ prefixes in the metadata
> output and arguments, it's good because it matches the api naming
> on the other hand it feels a bit overkill

I don't really like just saying 'id', because it feels like an
over-used 2 characters. However, I can strip them.

>
> - I'm of the school that would have already tried to use classes
> for each command and tried to avoid some of the boilerplate, mostly
> I worry that setup_arg_parser is borderline in size

I just don't know of a good framework for integrating class-based
subcommands with argparse.

bzrlib has a very nice one, but it isn't a generic available thing,
and I don't want a dependency on bzrlib. Commandant is an attempt to
pull out bzrlib's stuff into a separate lib, but I don't think it is
particularly actively maintained.

I'll do something as a follow up to this.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk6ypU8ACgkQJdeBCYSNAAMpJQCglsPmN0RljNoCnKovQRA0h4GP
g+EAoKYrEVBEDE3EdU8u1B1HipN/rCxK
=vnc3
-----END PGP SIGNATURE-----

102. By John A Meinel

Change the content so metadata always goes to stderr.

103. By John A Meinel

Calling things 'rev:' and 'id:' rather than 'doc_rev:' in the commandline output.

Revision history for this message
Samuele Pedroni (pedronis) wrote :

+1,

wondering if:

        parser.add_argument('--doc-id', default=None,
            help='Set the document identifier')

should be:

        parser.add_argument('--id', default=None, dest='doc_id',
            help='Set the document identifier')

as well or not

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

It probably fits better, though it isn't how "put_doc" names the argument.
Could probably do it there, too.

John
=:->
On Nov 3, 2011 6:13 PM, "Samuele Pedroni" <email address hidden>
wrote:

> Review: Approve
>
> +1,
>
> wondering if:
>
> parser.add_argument('--doc-id', default=None,
> help='Set the document identifier')
>
> should be:
>
> parser.add_argument('--id', default=None, dest='doc_id',
> help='Set the document identifier')
>
> as well or not
> --
>
> https://code.launchpad.net/~jameinel/u1db/u1db-client-commandline/+merge/80855
> You are the owner of lp:~jameinel/u1db/u1db-client-commandline.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'u1db-client'
2--- u1db-client 1970-01-01 00:00:00 +0000
3+++ u1db-client 2011-11-03 14:41:29 +0000
4@@ -0,0 +1,22 @@
5+#!/usr/bin/env python
6+# Copyright 2011 Canonical Ltd.
7+#
8+# This program is free software: you can redistribute it and/or modify it
9+# under the terms of the GNU General Public License version 3, as published
10+# by the Free Software Foundation.
11+#
12+# This program is distributed in the hope that it will be useful, but
13+# WITHOUT ANY WARRANTY; without even the implied warranties of
14+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15+# PURPOSE. See the GNU General Public License for more details.
16+#
17+# You should have received a copy of the GNU General Public License along
18+# with this program. If not, see <http://www.gnu.org/licenses/>.
19+
20+import sys
21+from u1db.commandline import client
22+
23+if __name__ == '__main__':
24+ sys.exit(client.main(sys.argv[1:]))
25+
26+# vim: ft=python
27
28=== added directory 'u1db/commandline'
29=== added file 'u1db/commandline/__init__.py'
30--- u1db/commandline/__init__.py 1970-01-01 00:00:00 +0000
31+++ u1db/commandline/__init__.py 2011-11-03 14:41:29 +0000
32@@ -0,0 +1,14 @@
33+# Copyright 2011 Canonical Ltd.
34+#
35+# This program is free software: you can redistribute it and/or modify it
36+# under the terms of the GNU General Public License version 3, as published
37+# by the Free Software Foundation.
38+#
39+# This program is distributed in the hope that it will be useful, but
40+# WITHOUT ANY WARRANTY; without even the implied warranties of
41+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
42+# PURPOSE. See the GNU General Public License for more details.
43+#
44+# You should have received a copy of the GNU General Public License along
45+# with this program. If not, see <http://www.gnu.org/licenses/>.
46+
47
48=== added file 'u1db/commandline/client.py'
49--- u1db/commandline/client.py 1970-01-01 00:00:00 +0000
50+++ u1db/commandline/client.py 2011-11-03 14:41:29 +0000
51@@ -0,0 +1,124 @@
52+# Copyright 2011 Canonical Ltd.
53+#
54+# This program is free software: you can redistribute it and/or modify it
55+# under the terms of the GNU General Public License version 3, as published
56+# by the Free Software Foundation.
57+#
58+# This program is distributed in the hope that it will be useful, but
59+# WITHOUT ANY WARRANTY; without even the implied warranties of
60+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
61+# PURPOSE. See the GNU General Public License for more details.
62+#
63+# You should have received a copy of the GNU General Public License along
64+# with this program. If not, see <http://www.gnu.org/licenses/>.
65+
66+"""Commandline bindings for the u1db-client program."""
67+
68+import argparse
69+import sys
70+
71+from u1db import (
72+ __version__ as _u1db_version,
73+ sync,
74+ )
75+from u1db.backends import sqlite_backend
76+from u1db.remote import (
77+ client,
78+ )
79+
80+
81+def cmd_create(database, doc_id, in_file, out_file, err_file):
82+ """Run 'create_doc'."""
83+ db = sqlite_backend.SQLiteDatabase.open_database(database)
84+ doc_id, doc_rev = db.create_doc(in_file.read(), doc_id=doc_id)
85+ err_file.write('id: %s\nrev: %s\n' % (doc_id, doc_rev))
86+
87+
88+def client_create(args):
89+ return cmd_create(args.database, args.doc_id, args.infile, sys.stdout,
90+ sys.stderr)
91+
92+
93+def cmd_get(database, doc_id, out_file, err_file):
94+ """actually run 'get_doc' on the given parameters"""
95+ db = sqlite_backend.SQLiteDatabase.open_database(database)
96+ doc_rev, doc, has_conflicts = db.get_doc(doc_id)
97+ out_file.write(doc)
98+ err_file.write('rev: %s\n' % (doc_rev,))
99+ if has_conflicts:
100+ pass
101+
102+
103+def client_get(args):
104+ """Run 'get_doc' for this client"""
105+ return cmd_get(args.database, args.doc_id, args.outfile, sys.stderr)
106+
107+
108+def cmd_put(database, doc_id, old_doc_rev, in_file, out_file, err_file):
109+ """run 'put_doc' and update the data."""
110+ db = sqlite_backend.SQLiteDatabase.open_database(database)
111+ doc_rev = db.put_doc(doc_id, old_doc_rev, in_file.read())
112+ err_file.write('rev: %s\n' % (doc_rev,))
113+
114+
115+def client_put(args):
116+ """Run 'put_doc'"""
117+ return cmd_put(args.database, args.doc_id, args.doc_rev, args.infile,
118+ sys.stdout, sys.stderr)
119+
120+
121+def cmd_sync(source_db, target_db):
122+ """Start a Sync request."""
123+ source = sqlite_backend.SQLiteDatabase.open_database(source_db)
124+ target = sqlite_backend.SQLiteDatabase.open_database(target_db)
125+ st = target.get_sync_target()
126+ syncer = sync.Synchronizer(source, st)
127+ syncer.sync()
128+
129+
130+def client_sync(args):
131+ """Run sync"""
132+ return cmd_sync(args.source, args.target)
133+
134+
135+def setup_arg_parser():
136+ p = argparse.ArgumentParser(description='Run actions from the U1DB client')
137+ p.add_argument('-V', '--version', action='version', version=_u1db_version)
138+ p.add_argument('-v', '--verbose', action='store_true', help='be chatty')
139+ subs = p.add_subparsers(title='commands')
140+ parser_create = subs.add_parser('create',
141+ help='Create a new document from scratch')
142+ parser_create.add_argument('database', help='The database to update')
143+ parser_create.add_argument('infile', nargs='?', default=sys.stdin,
144+ help='The file to read content from.')
145+ parser_create.add_argument('--doc-id', default=None,
146+ help='Set the document identifier')
147+ parser_create.set_defaults(func=client_create)
148+ parser_get = subs.add_parser('get',
149+ help='Extract a document from the database')
150+ parser_get.add_argument('database', help='The database to query')
151+ parser_get.add_argument('doc_id', help='The document id to retrieve.')
152+ parser_get.add_argument('outfile', nargs='?', default=sys.stdout,
153+ help='The file to write the document to',
154+ type=argparse.FileType('wb'))
155+ parser_get.set_defaults(func=client_get)
156+ parser_put = subs.add_parser('put', help='Add a document to the database')
157+ parser_put.add_argument('database', help='The database to update')
158+ parser_put.add_argument('doc_id', help='The document id to retrieve')
159+ parser_put.add_argument('doc_rev',
160+ help='The revision of the document (which is being superseded.)')
161+ parser_put.add_argument('infile', nargs='?', default=sys.stdin,
162+ help='The filename of the document that will be used for content',
163+ type=argparse.FileType('rb'))
164+ parser_put.set_defaults(func=client_put)
165+ parser_sync = subs.add_parser('sync', help='Synchronize two databases')
166+ parser_sync.add_argument('source', help='database to sync from')
167+ parser_sync.add_argument('target', help='database to sync to')
168+ parser_sync.set_defaults(func=client_sync)
169+ return p
170+
171+
172+def main(args):
173+ p = setup_arg_parser()
174+ args = p.parse_args(args)
175+ return args.func(args)
176
177=== modified file 'u1db/tests/commandline/__init__.py'
178--- u1db/tests/commandline/__init__.py 2011-11-03 14:41:29 +0000
179+++ u1db/tests/commandline/__init__.py 2011-11-03 14:41:29 +0000
180@@ -12,4 +12,35 @@
181 # You should have received a copy of the GNU General Public License along
182 # with this program. If not, see <http://www.gnu.org/licenses/>.
183
184+import time
185+
186+
187+def safe_close(process, timeout=0.1):
188+ """Shutdown the process in the nicest fashion you can manage.
189+
190+ :param process: A subprocess.Popen object.
191+ :param timeout: We'll try to send 'SIGTERM' but if the process is alive
192+ longer that 'timeout', we'll send SIGKILL.
193+ """
194+ if process.poll() is not None:
195+ return
196+ try:
197+ process.terminate()
198+ except OSError, e:
199+ if e.errno in (errno.ESRCH,):
200+ # Process has exited
201+ return
202+ tend = time.time() + timeout
203+ while time.time() < tend:
204+ if process.poll() is not None:
205+ return
206+ time.sleep(0.01)
207+ try:
208+ process.kill()
209+ except OSError, e:
210+ if e.errno in (errno.ESRCH,):
211+ # Process has exited
212+ return
213+ process.wait()
214+
215
216
217=== added file 'u1db/tests/commandline/test_client.py'
218--- u1db/tests/commandline/test_client.py 1970-01-01 00:00:00 +0000
219+++ u1db/tests/commandline/test_client.py 2011-11-03 14:41:29 +0000
220@@ -0,0 +1,240 @@
221+# Copyright 2011 Canonical Ltd.
222+#
223+# This program is free software: you can redistribute it and/or modify it
224+# under the terms of the GNU General Public License version 3, as published
225+# by the Free Software Foundation.
226+#
227+# This program is distributed in the hope that it will be useful, but
228+# WITHOUT ANY WARRANTY; without even the implied warranties of
229+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
230+# PURPOSE. See the GNU General Public License for more details.
231+#
232+# You should have received a copy of the GNU General Public License along
233+# with this program. If not, see <http://www.gnu.org/licenses/>.
234+
235+import cStringIO
236+import os
237+import sys
238+import subprocess
239+import time
240+
241+from u1db import (
242+ __version__ as _u1db_version,
243+ tests,
244+ )
245+from u1db.backends import (
246+ sqlite_backend,
247+ )
248+from u1db.commandline import client
249+from u1db.tests.commandline import safe_close
250+
251+
252+class TestArgs(tests.TestCase):
253+
254+ def setUp(self):
255+ super(TestArgs, self).setUp()
256+ self.parser = client.setup_arg_parser()
257+
258+ def parse_args(self, args):
259+ # ArgumentParser.parse_args doesn't play very nicely with a test suite,
260+ # so we trap SystemExit in case something is wrong with the args we're
261+ # parsing.
262+ try:
263+ return self.parser.parse_args(args)
264+ except SystemExit, e:
265+ raise AssertionError('got SystemExit')
266+
267+
268+ def test_create(self):
269+ args = self.parse_args(['create', 'test.db'])
270+ self.assertEqual(client.client_create, args.func)
271+ self.assertEqual('test.db', args.database)
272+ self.assertEqual(None, args.doc_id)
273+ self.assertEqual(sys.stdin, args.infile)
274+
275+ def test_create_custom_doc_id(self):
276+ args = self.parse_args(['create', '--doc-id', 'xyz', 'test.db'])
277+ self.assertEqual(client.client_create, args.func)
278+ self.assertEqual('test.db', args.database)
279+ self.assertEqual('xyz', args.doc_id)
280+ self.assertEqual(sys.stdin, args.infile)
281+
282+ def test_get(self):
283+ args = self.parse_args(['get', 'test.db', 'doc-id'])
284+ self.assertEqual(client.client_get, args.func)
285+ self.assertEqual('test.db', args.database)
286+ self.assertEqual('doc-id', args.doc_id)
287+ self.assertEqual(sys.stdout, args.outfile)
288+
289+ def test_get_dash(self):
290+ args = self.parse_args(['get', 'test.db', 'doc-id', '-'])
291+ self.assertEqual(client.client_get, args.func)
292+ self.assertEqual('test.db', args.database)
293+ self.assertEqual('doc-id', args.doc_id)
294+ self.assertEqual(sys.stdout, args.outfile)
295+
296+ def test_put(self):
297+ args = self.parse_args(['put', 'test.db', 'doc-id', 'old-doc-rev'])
298+ self.assertEqual(client.client_put, args.func)
299+ self.assertEqual('test.db', args.database)
300+ self.assertEqual('doc-id', args.doc_id)
301+ self.assertEqual('old-doc-rev', args.doc_rev)
302+ self.assertEqual(sys.stdin, args.infile)
303+
304+ def test_sync(self):
305+ args = self.parse_args(['sync', 'source', 'target'])
306+ self.assertEqual(client.client_sync, args.func)
307+ self.assertEqual('source', args.source)
308+ self.assertEqual('target', args.target)
309+
310+
311+class TestCaseWithDB(tests.TestCase):
312+
313+ def setUp(self):
314+ super(TestCaseWithDB, self).setUp()
315+ self.working_dir = self.createTempDir()
316+ self.db_path = self.working_dir + '/test.db'
317+ self.db = sqlite_backend.SQLitePartialExpandDatabase(self.db_path)
318+ self.db._set_replica_uid('test')
319+
320+
321+class TestCmdCreate(TestCaseWithDB):
322+
323+ def test_create(self):
324+ out = cStringIO.StringIO()
325+ err = cStringIO.StringIO()
326+ inf = cStringIO.StringIO(tests.simple_doc)
327+ client.cmd_create(self.db_path, 'test-id', inf, out, err)
328+ doc_rev, doc, has_conflicts = self.db.get_doc('test-id')
329+ self.assertEqual(tests.simple_doc, doc)
330+ self.assertFalse(has_conflicts)
331+ self.assertEqual('', out.getvalue())
332+ self.assertEqual('id: test-id\nrev: %s\n' % (doc_rev,),
333+ err.getvalue())
334+
335+
336+class TestCmdGet(TestCaseWithDB):
337+
338+ def setUp(self):
339+ super(TestCmdGet, self).setUp()
340+ _, self.doc_rev = self.db.create_doc(tests.simple_doc,
341+ doc_id='my-test-doc')
342+
343+ def test_get_simple(self):
344+ out = cStringIO.StringIO()
345+ err = cStringIO.StringIO()
346+ client.cmd_get(self.db_path, 'my-test-doc', out_file=out, err_file=err)
347+ self.assertEqual(tests.simple_doc, out.getvalue())
348+ self.assertEqual('rev: %s\n' % (self.doc_rev,),
349+ err.getvalue())
350+
351+
352+class TestCmdPut(TestCaseWithDB):
353+
354+ def setUp(self):
355+ super(TestCmdPut, self).setUp()
356+ _, self.doc_rev = self.db.create_doc(tests.simple_doc,
357+ doc_id='my-test-doc')
358+
359+ def test_put_simple(self):
360+ inf = cStringIO.StringIO(tests.nested_doc)
361+ out = cStringIO.StringIO()
362+ err = cStringIO.StringIO()
363+ client.cmd_put(self.db_path, 'my-test-doc', self.doc_rev,
364+ inf, out, err)
365+ doc_rev, doc, has_conflicts = self.db.get_doc('my-test-doc')
366+ self.assertEqual(tests.nested_doc, doc)
367+ self.assertFalse(has_conflicts)
368+ self.assertEqual('', out.getvalue())
369+ self.assertEqual('rev: %s\n' % (doc_rev,),
370+ err.getvalue())
371+
372+
373+class TestCmdSync(TestCaseWithDB):
374+
375+ def setUp(self):
376+ super(TestCmdSync, self).setUp()
377+ self.db2_path = self.working_dir + '/test2.db'
378+ self.db2 = sqlite_backend.SQLitePartialExpandDatabase(self.db2_path)
379+ self.db2._set_replica_uid('test2')
380+ _, self.doc_rev = self.db.create_doc(tests.simple_doc,
381+ doc_id='test-id')
382+ _, self.doc2_rev = self.db2.create_doc(tests.nested_doc,
383+ doc_id='my-test-id')
384+
385+ def test_sync(self):
386+ client.cmd_sync(self.db_path, self.db2_path)
387+ self.assertEqual((self.doc_rev, tests.simple_doc, False),
388+ self.db2.get_doc('test-id'))
389+ self.assertEqual((self.doc2_rev, tests.nested_doc, False),
390+ self.db.get_doc('my-test-id'))
391+
392+
393+class TestCommandLine(TestCaseWithDB):
394+
395+ def _get_u1db_client_path(self):
396+ from u1db import __path__ as u1db_path
397+ u1db_parent_dir = os.path.dirname(u1db_path[0])
398+ return os.path.join(u1db_parent_dir, 'u1db-client')
399+
400+ def runU1DBClient(self, args):
401+ command = [sys.executable, self._get_u1db_client_path()]
402+ command.extend(args)
403+ p = subprocess.Popen(command, stdin=subprocess.PIPE,
404+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
405+ self.addCleanup(safe_close, p)
406+ return p
407+
408+ def run_main(self, args, stdin=None):
409+ if stdin is not None:
410+ self.patch(sys, 'stdin', cStringIO.StringIO(stdin))
411+ stdout = cStringIO.StringIO()
412+ stderr = cStringIO.StringIO()
413+ self.patch(sys, 'stdout', stdout)
414+ self.patch(sys, 'stderr', stderr)
415+ ret = client.main(args)
416+ if ret is None:
417+ ret = 0
418+ return ret, stdout.getvalue(), stderr.getvalue()
419+
420+ def test_create_subprocess(self):
421+ p = self.runU1DBClient(['create', '--doc-id', 'test-id', self.db_path])
422+ stdout, stderr = p.communicate(tests.simple_doc)
423+ self.assertEqual(0, p.returncode)
424+ self.assertEqual('', stdout)
425+ doc_rev, doc, has_conflicts = self.db.get_doc('test-id')
426+ self.assertEqual(tests.simple_doc, doc)
427+ self.assertFalse(has_conflicts)
428+ self.assertEqual('id: test-id\nrev: %s\n' % (doc_rev,),
429+ stderr)
430+
431+ def test_get(self):
432+ _, doc_rev = self.db.create_doc(tests.simple_doc, doc_id='test-id')
433+ ret, stdout, stderr = self.run_main(['get', self.db_path, 'test-id'])
434+ self.assertEqual(0, ret)
435+ self.assertEqual(tests.simple_doc, stdout)
436+ self.assertEqual('rev: %s\n' % (doc_rev,), stderr)
437+
438+ def test_put(self):
439+ _, doc_rev = self.db.create_doc(tests.simple_doc, doc_id='test-id')
440+ ret, stdout, stderr = self.run_main(
441+ ['put', self.db_path, 'test-id', doc_rev],
442+ stdin=tests.nested_doc)
443+ doc_rev, doc, has_conflicts = self.db.get_doc('test-id')
444+ self.assertFalse(has_conflicts)
445+ self.assertEqual(tests.nested_doc, doc)
446+ self.assertEqual(0, ret)
447+ self.assertEqual('', stdout)
448+ self.assertEqual('rev: %s\n' % (doc_rev,), stderr)
449+
450+ def test_sync(self):
451+ _, doc_rev = self.db.create_doc(tests.simple_doc, doc_id='test-id')
452+ self.db2_path = self.working_dir + '/test2.db'
453+ self.db2 = sqlite_backend.SQLitePartialExpandDatabase(self.db2_path)
454+ ret, stdout, stderr = self.run_main(
455+ ['sync', self.db_path, self.db2_path])
456+ self.assertEqual(0, ret)
457+ self.assertEqual('', stdout)
458+ self.assertEqual('', stderr)
459+ self.assertEqual((doc_rev, tests.simple_doc, False),
460+ self.db2.get_doc('test-id'))
461
462=== modified file 'u1db/tests/commandline/test_serve.py'
463--- u1db/tests/commandline/test_serve.py 2011-11-03 14:41:29 +0000
464+++ u1db/tests/commandline/test_serve.py 2011-11-03 14:41:29 +0000
465@@ -17,42 +17,13 @@
466 import socket
467 import subprocess
468 import sys
469-import time
470
471 from u1db import (
472 __version__ as _u1db_version,
473 tests,
474 )
475 from u1db.remote import client
476-
477-
478-def safe_close(process, timeout=0.1):
479- """Shutdown the process in the nicest fashion you can manage.
480-
481- :param process: A subprocess.Popen object.
482- :param timeout: We'll try to send 'SIGTERM' but if the process is alive
483- longer that 'timeout', we'll send SIGKILL.
484- """
485- if process.poll() is not None:
486- return
487- try:
488- process.terminate()
489- except OSError, e:
490- if e.errno in (errno.ESRCH,):
491- # Process has exited
492- return
493- tend = time.time() + timeout
494- while time.time() < tend:
495- if process.poll() is not None:
496- return
497- time.sleep(0.01)
498- try:
499- process.kill()
500- except OSError, e:
501- if e.errno in (errno.ESRCH,):
502- # Process has exited
503- return
504- process.wait()
505+from u1db.tests.commandline import safe_close
506
507
508 class TestU1DBServe(tests.TestCase):

Subscribers

People subscribed via source and target branches