Merge lp:~jameinel/u1db/u1db-client-commandline into lp:u1db
- u1db-client-commandline
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Samuele Pedroni | Approve | ||
Review via email: mp+80855@code.launchpad.net |
Commit message
Description of the change
This implements a 'u1db-client' commandline utility.
This allows us to do 'u1db-client get/put/
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.
Samuele Pedroni (pedronis) wrote : | # |
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://
iEYEARECAAYFAk6
g+EAoKYrEVBEDE3
=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.
Samuele Pedroni (pedronis) wrote : | # |
+1,
wondering if:
should be:
as well or not
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.
> help='Set the document identifier')
>
> should be:
>
> parser.
> help='Set the document identifier')
>
> as well or not
> --
>
> https:/
> You are the owner of lp:~jameinel/u1db/u1db-client-commandline.
>
Preview Diff
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): |
- 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