]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_cli.py
cli: improve formatting of all commands
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_cli.py
1
2 import os.path
3 from twisted.trial import unittest
4 from cStringIO import StringIO
5 import urllib, sys
6
7 from mock import Mock, call
8
9 import allmydata
10 from allmydata.util import fileutil, hashutil, base32, keyutil
11 from allmydata import uri
12 from allmydata.immutable import upload
13 from allmydata.dirnode import normalize
14 from allmydata.scripts.common_http import socket_error
15 import allmydata.scripts.common_http
16 from pycryptopp.publickey import ed25519
17
18 # Test that the scripts can be imported.
19 from allmydata.scripts import create_node, debug, keygen, startstop_node, \
20     tahoe_add_alias, tahoe_backup, tahoe_check, tahoe_cp, tahoe_get, tahoe_ls, \
21     tahoe_manifest, tahoe_mkdir, tahoe_mv, tahoe_put, tahoe_unlink, tahoe_webopen
22 _hush_pyflakes = [create_node, debug, keygen, startstop_node,
23     tahoe_add_alias, tahoe_backup, tahoe_check, tahoe_cp, tahoe_get, tahoe_ls,
24     tahoe_manifest, tahoe_mkdir, tahoe_mv, tahoe_put, tahoe_unlink, tahoe_webopen]
25
26 from allmydata.scripts import common
27 from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases, get_alias, \
28      DefaultAliasMarker
29
30 from allmydata.scripts import cli, debug, runner
31 from allmydata.test.common_util import ReallyEqualMixin
32 from allmydata.test.no_network import GridTestMixin
33 from twisted.internet import threads # CLI tests use deferToThread
34 from twisted.python import usage
35
36 from allmydata.util.assertutil import precondition
37 from allmydata.util.encodingutil import listdir_unicode, unicode_platform, \
38     get_io_encoding, get_filesystem_encoding
39
40 timeout = 480 # deep_check takes 360s on Zandr's linksys box, others take > 240s
41
42 def parse_options(basedir, command, args):
43     o = runner.Options()
44     o.parseOptions(["--node-directory", basedir, command] + args)
45     while hasattr(o, "subOptions"):
46         o = o.subOptions
47     return o
48
49 class CLITestMixin(ReallyEqualMixin):
50     def do_cli(self, verb, *args, **kwargs):
51         nodeargs = [
52             "--node-directory", self.get_clientdir(),
53             ]
54         argv = nodeargs + [verb] + list(args)
55         stdin = kwargs.get("stdin", "")
56         stdout, stderr = StringIO(), StringIO()
57         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
58                                   stdin=StringIO(stdin),
59                                   stdout=stdout, stderr=stderr)
60         def _done(rc):
61             return rc, stdout.getvalue(), stderr.getvalue()
62         d.addCallback(_done)
63         return d
64
65     def skip_if_cannot_represent_filename(self, u):
66         precondition(isinstance(u, unicode))
67
68         enc = get_filesystem_encoding()
69         if not unicode_platform():
70             try:
71                 u.encode(enc)
72             except UnicodeEncodeError:
73                 raise unittest.SkipTest("A non-ASCII filename could not be encoded on this platform.")
74
75
76 class CLI(CLITestMixin, unittest.TestCase):
77     def _dump_cap(self, *args):
78         config = debug.DumpCapOptions()
79         config.stdout,config.stderr = StringIO(), StringIO()
80         config.parseOptions(args)
81         debug.dump_cap(config)
82         self.failIf(config.stderr.getvalue())
83         output = config.stdout.getvalue()
84         return output
85
86     def test_dump_cap_chk(self):
87         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
88         uri_extension_hash = hashutil.uri_extension_hash("stuff")
89         needed_shares = 25
90         total_shares = 100
91         size = 1234
92         u = uri.CHKFileURI(key=key,
93                            uri_extension_hash=uri_extension_hash,
94                            needed_shares=needed_shares,
95                            total_shares=total_shares,
96                            size=size)
97         output = self._dump_cap(u.to_string())
98         self.failUnless("CHK File:" in output, output)
99         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
100         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
101         self.failUnless("size: 1234" in output, output)
102         self.failUnless("k/N: 25/100" in output, output)
103         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
104
105         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
106                                 u.to_string())
107         self.failUnless("client renewal secret: znxmki5zdibb5qlt46xbdvk2t55j7hibejq3i5ijyurkr6m6jkhq" in output, output)
108
109         output = self._dump_cap(u.get_verify_cap().to_string())
110         self.failIf("key: " in output, output)
111         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
112         self.failUnless("size: 1234" in output, output)
113         self.failUnless("k/N: 25/100" in output, output)
114         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
115
116         prefixed_u = "http://127.0.0.1/uri/%s" % urllib.quote(u.to_string())
117         output = self._dump_cap(prefixed_u)
118         self.failUnless("CHK File:" in output, output)
119         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
120         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
121         self.failUnless("size: 1234" in output, output)
122         self.failUnless("k/N: 25/100" in output, output)
123         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
124
125     def test_dump_cap_lit(self):
126         u = uri.LiteralFileURI("this is some data")
127         output = self._dump_cap(u.to_string())
128         self.failUnless("Literal File URI:" in output, output)
129         self.failUnless("data: 'this is some data'" in output, output)
130
131     def test_dump_cap_sdmf(self):
132         writekey = "\x01" * 16
133         fingerprint = "\xfe" * 32
134         u = uri.WriteableSSKFileURI(writekey, fingerprint)
135
136         output = self._dump_cap(u.to_string())
137         self.failUnless("SDMF Writeable URI:" in output, output)
138         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output, output)
139         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
140         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
141         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
142
143         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
144                                 u.to_string())
145         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
146
147         fileutil.make_dirs("cli/test_dump_cap/private")
148         fileutil.write("cli/test_dump_cap/private/secret", "5s33nk3qpvnj2fw3z4mnm2y6fa\n")
149         output = self._dump_cap("--client-dir", "cli/test_dump_cap",
150                                 u.to_string())
151         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
152
153         output = self._dump_cap("--client-dir", "cli/test_dump_cap_BOGUS",
154                                 u.to_string())
155         self.failIf("file renewal secret:" in output, output)
156
157         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
158                                 u.to_string())
159         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
160         self.failIf("file renewal secret:" in output, output)
161
162         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
163                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
164                                 u.to_string())
165         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
166         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
167         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
168
169         u = u.get_readonly()
170         output = self._dump_cap(u.to_string())
171         self.failUnless("SDMF Read-only URI:" in output, output)
172         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
173         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
174         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
175
176         u = u.get_verify_cap()
177         output = self._dump_cap(u.to_string())
178         self.failUnless("SDMF Verifier URI:" in output, output)
179         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
180         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
181
182     def test_dump_cap_mdmf(self):
183         writekey = "\x01" * 16
184         fingerprint = "\xfe" * 32
185         u = uri.WriteableMDMFFileURI(writekey, fingerprint)
186
187         output = self._dump_cap(u.to_string())
188         self.failUnless("MDMF Writeable URI:" in output, output)
189         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output, output)
190         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
191         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
192         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
193
194         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
195                                 u.to_string())
196         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
197
198         fileutil.make_dirs("cli/test_dump_cap/private")
199         fileutil.write("cli/test_dump_cap/private/secret", "5s33nk3qpvnj2fw3z4mnm2y6fa\n")
200         output = self._dump_cap("--client-dir", "cli/test_dump_cap",
201                                 u.to_string())
202         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
203
204         output = self._dump_cap("--client-dir", "cli/test_dump_cap_BOGUS",
205                                 u.to_string())
206         self.failIf("file renewal secret:" in output, output)
207
208         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
209                                 u.to_string())
210         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
211         self.failIf("file renewal secret:" in output, output)
212
213         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
214                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
215                                 u.to_string())
216         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
217         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
218         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
219
220         u = u.get_readonly()
221         output = self._dump_cap(u.to_string())
222         self.failUnless("MDMF Read-only URI:" in output, output)
223         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
224         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
225         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
226
227         u = u.get_verify_cap()
228         output = self._dump_cap(u.to_string())
229         self.failUnless("MDMF Verifier URI:" in output, output)
230         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
231         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
232
233
234     def test_dump_cap_chk_directory(self):
235         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
236         uri_extension_hash = hashutil.uri_extension_hash("stuff")
237         needed_shares = 25
238         total_shares = 100
239         size = 1234
240         u1 = uri.CHKFileURI(key=key,
241                             uri_extension_hash=uri_extension_hash,
242                             needed_shares=needed_shares,
243                             total_shares=total_shares,
244                             size=size)
245         u = uri.ImmutableDirectoryURI(u1)
246
247         output = self._dump_cap(u.to_string())
248         self.failUnless("CHK Directory URI:" in output, output)
249         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
250         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
251         self.failUnless("size: 1234" in output, output)
252         self.failUnless("k/N: 25/100" in output, output)
253         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
254
255         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
256                                 u.to_string())
257         self.failUnless("file renewal secret: csrvkjgomkyyyil5yo4yk5np37p6oa2ve2hg6xmk2dy7kaxsu6xq" in output, output)
258
259         u = u.get_verify_cap()
260         output = self._dump_cap(u.to_string())
261         self.failUnless("CHK Directory Verifier URI:" in output, output)
262         self.failIf("key: " in output, output)
263         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
264         self.failUnless("size: 1234" in output, output)
265         self.failUnless("k/N: 25/100" in output, output)
266         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
267
268     def test_dump_cap_sdmf_directory(self):
269         writekey = "\x01" * 16
270         fingerprint = "\xfe" * 32
271         u1 = uri.WriteableSSKFileURI(writekey, fingerprint)
272         u = uri.DirectoryURI(u1)
273
274         output = self._dump_cap(u.to_string())
275         self.failUnless("Directory Writeable URI:" in output, output)
276         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output,
277                         output)
278         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
279         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output,
280                         output)
281         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
282
283         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
284                                 u.to_string())
285         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
286
287         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
288                                 u.to_string())
289         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
290         self.failIf("file renewal secret:" in output, output)
291
292         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
293                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
294                                 u.to_string())
295         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
296         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
297         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
298
299         u = u.get_readonly()
300         output = self._dump_cap(u.to_string())
301         self.failUnless("Directory Read-only URI:" in output, output)
302         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
303         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
304         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
305
306         u = u.get_verify_cap()
307         output = self._dump_cap(u.to_string())
308         self.failUnless("Directory Verifier URI:" in output, output)
309         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
310         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
311
312     def test_dump_cap_mdmf_directory(self):
313         writekey = "\x01" * 16
314         fingerprint = "\xfe" * 32
315         u1 = uri.WriteableMDMFFileURI(writekey, fingerprint)
316         u = uri.MDMFDirectoryURI(u1)
317
318         output = self._dump_cap(u.to_string())
319         self.failUnless("Directory Writeable URI:" in output, output)
320         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output,
321                         output)
322         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
323         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output,
324                         output)
325         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
326
327         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
328                                 u.to_string())
329         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
330
331         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
332                                 u.to_string())
333         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
334         self.failIf("file renewal secret:" in output, output)
335
336         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
337                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
338                                 u.to_string())
339         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
340         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
341         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
342
343         u = u.get_readonly()
344         output = self._dump_cap(u.to_string())
345         self.failUnless("Directory Read-only URI:" in output, output)
346         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
347         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
348         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
349
350         u = u.get_verify_cap()
351         output = self._dump_cap(u.to_string())
352         self.failUnless("Directory Verifier URI:" in output, output)
353         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
354         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
355
356
357     def _catalog_shares(self, *basedirs):
358         o = debug.CatalogSharesOptions()
359         o.stdout,o.stderr = StringIO(), StringIO()
360         args = list(basedirs)
361         o.parseOptions(args)
362         debug.catalog_shares(o)
363         out = o.stdout.getvalue()
364         err = o.stderr.getvalue()
365         return out, err
366
367     def test_catalog_shares_error(self):
368         nodedir1 = "cli/test_catalog_shares/node1"
369         sharedir = os.path.join(nodedir1, "storage", "shares", "mq", "mqfblse6m5a6dh45isu2cg7oji")
370         fileutil.make_dirs(sharedir)
371         fileutil.write("cli/test_catalog_shares/node1/storage/shares/mq/not-a-dir", "")
372         # write a bogus share that looks a little bit like CHK
373         fileutil.write(os.path.join(sharedir, "8"),
374                        "\x00\x00\x00\x01" + "\xff" * 200) # this triggers an assert
375
376         nodedir2 = "cli/test_catalog_shares/node2"
377         fileutil.make_dirs(nodedir2)
378         fileutil.write("cli/test_catalog_shares/node1/storage/shares/not-a-dir", "")
379
380         # now make sure that the 'catalog-shares' commands survives the error
381         out, err = self._catalog_shares(nodedir1, nodedir2)
382         self.failUnlessReallyEqual(out, "", out)
383         self.failUnless("Error processing " in err,
384                         "didn't see 'error processing' in '%s'" % err)
385         #self.failUnless(nodedir1 in err,
386         #                "didn't see '%s' in '%s'" % (nodedir1, err))
387         # windows mangles the path, and os.path.join isn't enough to make
388         # up for it, so just look for individual strings
389         self.failUnless("node1" in err,
390                         "didn't see 'node1' in '%s'" % err)
391         self.failUnless("mqfblse6m5a6dh45isu2cg7oji" in err,
392                         "didn't see 'mqfblse6m5a6dh45isu2cg7oji' in '%s'" % err)
393
394     def test_alias(self):
395         def s128(c): return base32.b2a(c*(128/8))
396         def s256(c): return base32.b2a(c*(256/8))
397         TA = "URI:DIR2:%s:%s" % (s128("T"), s256("T"))
398         WA = "URI:DIR2:%s:%s" % (s128("W"), s256("W"))
399         CA = "URI:DIR2:%s:%s" % (s128("C"), s256("C"))
400         aliases = {"tahoe": TA,
401                    "work": WA,
402                    "c": CA}
403         def ga1(path):
404             return get_alias(aliases, path, u"tahoe")
405         uses_lettercolon = common.platform_uses_lettercolon_drivename()
406         self.failUnlessReallyEqual(ga1(u"bare"), (TA, "bare"))
407         self.failUnlessReallyEqual(ga1(u"baredir/file"), (TA, "baredir/file"))
408         self.failUnlessReallyEqual(ga1(u"baredir/file:7"), (TA, "baredir/file:7"))
409         self.failUnlessReallyEqual(ga1(u"tahoe:"), (TA, ""))
410         self.failUnlessReallyEqual(ga1(u"tahoe:file"), (TA, "file"))
411         self.failUnlessReallyEqual(ga1(u"tahoe:dir/file"), (TA, "dir/file"))
412         self.failUnlessReallyEqual(ga1(u"work:"), (WA, ""))
413         self.failUnlessReallyEqual(ga1(u"work:file"), (WA, "file"))
414         self.failUnlessReallyEqual(ga1(u"work:dir/file"), (WA, "dir/file"))
415         # default != None means we really expect a tahoe path, regardless of
416         # whether we're on windows or not. This is what 'tahoe get' uses.
417         self.failUnlessReallyEqual(ga1(u"c:"), (CA, ""))
418         self.failUnlessReallyEqual(ga1(u"c:file"), (CA, "file"))
419         self.failUnlessReallyEqual(ga1(u"c:dir/file"), (CA, "dir/file"))
420         self.failUnlessReallyEqual(ga1(u"URI:stuff"), ("URI:stuff", ""))
421         self.failUnlessReallyEqual(ga1(u"URI:stuff/file"), ("URI:stuff", "file"))
422         self.failUnlessReallyEqual(ga1(u"URI:stuff:./file"), ("URI:stuff", "file"))
423         self.failUnlessReallyEqual(ga1(u"URI:stuff/dir/file"), ("URI:stuff", "dir/file"))
424         self.failUnlessReallyEqual(ga1(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
425         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:")
426         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:dir")
427         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:dir/file")
428
429         def ga2(path):
430             return get_alias(aliases, path, None)
431         self.failUnlessReallyEqual(ga2(u"bare"), (DefaultAliasMarker, "bare"))
432         self.failUnlessReallyEqual(ga2(u"baredir/file"),
433                              (DefaultAliasMarker, "baredir/file"))
434         self.failUnlessReallyEqual(ga2(u"baredir/file:7"),
435                              (DefaultAliasMarker, "baredir/file:7"))
436         self.failUnlessReallyEqual(ga2(u"baredir/sub:1/file:7"),
437                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
438         self.failUnlessReallyEqual(ga2(u"tahoe:"), (TA, ""))
439         self.failUnlessReallyEqual(ga2(u"tahoe:file"), (TA, "file"))
440         self.failUnlessReallyEqual(ga2(u"tahoe:dir/file"), (TA, "dir/file"))
441         # on windows, we really want c:foo to indicate a local file.
442         # default==None is what 'tahoe cp' uses.
443         if uses_lettercolon:
444             self.failUnlessReallyEqual(ga2(u"c:"), (DefaultAliasMarker, "c:"))
445             self.failUnlessReallyEqual(ga2(u"c:file"), (DefaultAliasMarker, "c:file"))
446             self.failUnlessReallyEqual(ga2(u"c:dir/file"),
447                                  (DefaultAliasMarker, "c:dir/file"))
448         else:
449             self.failUnlessReallyEqual(ga2(u"c:"), (CA, ""))
450             self.failUnlessReallyEqual(ga2(u"c:file"), (CA, "file"))
451             self.failUnlessReallyEqual(ga2(u"c:dir/file"), (CA, "dir/file"))
452         self.failUnlessReallyEqual(ga2(u"work:"), (WA, ""))
453         self.failUnlessReallyEqual(ga2(u"work:file"), (WA, "file"))
454         self.failUnlessReallyEqual(ga2(u"work:dir/file"), (WA, "dir/file"))
455         self.failUnlessReallyEqual(ga2(u"URI:stuff"), ("URI:stuff", ""))
456         self.failUnlessReallyEqual(ga2(u"URI:stuff/file"), ("URI:stuff", "file"))
457         self.failUnlessReallyEqual(ga2(u"URI:stuff:./file"), ("URI:stuff", "file"))
458         self.failUnlessReallyEqual(ga2(u"URI:stuff/dir/file"), ("URI:stuff", "dir/file"))
459         self.failUnlessReallyEqual(ga2(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
460         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:")
461         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:dir")
462         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:dir/file")
463
464         def ga3(path):
465             old = common.pretend_platform_uses_lettercolon
466             try:
467                 common.pretend_platform_uses_lettercolon = True
468                 retval = get_alias(aliases, path, None)
469             finally:
470                 common.pretend_platform_uses_lettercolon = old
471             return retval
472         self.failUnlessReallyEqual(ga3(u"bare"), (DefaultAliasMarker, "bare"))
473         self.failUnlessReallyEqual(ga3(u"baredir/file"),
474                              (DefaultAliasMarker, "baredir/file"))
475         self.failUnlessReallyEqual(ga3(u"baredir/file:7"),
476                              (DefaultAliasMarker, "baredir/file:7"))
477         self.failUnlessReallyEqual(ga3(u"baredir/sub:1/file:7"),
478                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
479         self.failUnlessReallyEqual(ga3(u"tahoe:"), (TA, ""))
480         self.failUnlessReallyEqual(ga3(u"tahoe:file"), (TA, "file"))
481         self.failUnlessReallyEqual(ga3(u"tahoe:dir/file"), (TA, "dir/file"))
482         self.failUnlessReallyEqual(ga3(u"c:"), (DefaultAliasMarker, "c:"))
483         self.failUnlessReallyEqual(ga3(u"c:file"), (DefaultAliasMarker, "c:file"))
484         self.failUnlessReallyEqual(ga3(u"c:dir/file"),
485                              (DefaultAliasMarker, "c:dir/file"))
486         self.failUnlessReallyEqual(ga3(u"work:"), (WA, ""))
487         self.failUnlessReallyEqual(ga3(u"work:file"), (WA, "file"))
488         self.failUnlessReallyEqual(ga3(u"work:dir/file"), (WA, "dir/file"))
489         self.failUnlessReallyEqual(ga3(u"URI:stuff"), ("URI:stuff", ""))
490         self.failUnlessReallyEqual(ga3(u"URI:stuff:./file"), ("URI:stuff", "file"))
491         self.failUnlessReallyEqual(ga3(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
492         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:")
493         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:dir")
494         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:dir/file")
495         # calling get_alias with a path that doesn't include an alias and
496         # default set to something that isn't in the aliases argument should
497         # raise an UnknownAliasError.
498         def ga4(path):
499             return get_alias(aliases, path, u"badddefault:")
500         self.failUnlessRaises(common.UnknownAliasError, ga4, u"afile")
501         self.failUnlessRaises(common.UnknownAliasError, ga4, u"a/dir/path/")
502
503         def ga5(path):
504             old = common.pretend_platform_uses_lettercolon
505             try:
506                 common.pretend_platform_uses_lettercolon = True
507                 retval = get_alias(aliases, path, u"baddefault:")
508             finally:
509                 common.pretend_platform_uses_lettercolon = old
510             return retval
511         self.failUnlessRaises(common.UnknownAliasError, ga5, u"C:\\Windows")
512
513     def test_alias_tolerance(self):
514         def s128(c): return base32.b2a(c*(128/8))
515         def s256(c): return base32.b2a(c*(256/8))
516         TA = "URI:DIR2:%s:%s" % (s128("T"), s256("T"))
517         aliases = {"present": TA,
518                    "future": "URI-FROM-FUTURE:ooh:aah"}
519         def ga1(path):
520             return get_alias(aliases, path, u"tahoe")
521         self.failUnlessReallyEqual(ga1(u"present:file"), (TA, "file"))
522         # this throws, via assert IDirnodeURI.providedBy(), since get_alias()
523         # wants a dirnode, and the future cap gives us UnknownURI instead.
524         self.failUnlessRaises(AssertionError, ga1, u"future:stuff")
525
526     def test_listdir_unicode_good(self):
527         filenames = [u'L\u00F4zane', u'Bern', u'Gen\u00E8ve']  # must be NFC
528
529         for name in filenames:
530             self.skip_if_cannot_represent_filename(name)
531
532         basedir = "cli/common/listdir_unicode_good"
533         fileutil.make_dirs(basedir)
534
535         for name in filenames:
536             open(os.path.join(unicode(basedir), name), "wb").close()
537
538         for file in listdir_unicode(unicode(basedir)):
539             self.failUnlessIn(normalize(file), filenames)
540
541     def test_exception_catcher(self):
542         self.basedir = "cli/exception_catcher"
543
544         runner_mock = Mock()
545         sys_exit_mock = Mock()
546         stderr = StringIO()
547         self.patch(sys, "argv", ["tahoe"])
548         self.patch(runner, "runner", runner_mock)
549         self.patch(sys, "exit", sys_exit_mock)
550         self.patch(sys, "stderr", stderr)
551         exc = Exception("canary")
552
553         def call_runner(args, install_node_control=True):
554             raise exc
555         runner_mock.side_effect = call_runner
556
557         runner.run()
558         self.failUnlessEqual(runner_mock.call_args_list, [call([], install_node_control=True)])
559         self.failUnlessEqual(sys_exit_mock.call_args_list, [call(1)])
560         self.failUnlessIn(str(exc), stderr.getvalue())
561
562
563 class Help(unittest.TestCase):
564     def test_get(self):
565         help = str(cli.GetOptions())
566         self.failUnlessIn("[options] REMOTE_FILE LOCAL_FILE", help)
567         self.failUnlessIn("% tahoe get FOO |less", help)
568
569     def test_put(self):
570         help = str(cli.PutOptions())
571         self.failUnlessIn("[options] LOCAL_FILE REMOTE_FILE", help)
572         self.failUnlessIn("% cat FILE | tahoe put", help)
573
574     def test_ls(self):
575         help = str(cli.ListOptions())
576         self.failUnlessIn("[options] [PATH]", help)
577
578     def test_unlink(self):
579         help = str(cli.UnlinkOptions())
580         self.failUnlessIn("[options] REMOTE_FILE", help)
581
582     def test_rm(self):
583         help = str(cli.RmOptions())
584         self.failUnlessIn("[options] REMOTE_FILE", help)
585
586     def test_mv(self):
587         help = str(cli.MvOptions())
588         self.failUnlessIn("[options] FROM TO", help)
589         self.failUnlessIn("Use 'tahoe mv' to move files", help)
590
591     def test_cp(self):
592         help = str(cli.CpOptions())
593         self.failUnlessIn("[options] FROM.. TO", help)
594         self.failUnlessIn("Use 'tahoe cp' to copy files", help)
595
596     def test_ln(self):
597         help = str(cli.LnOptions())
598         self.failUnlessIn("[options] FROM_LINK TO_LINK", help)
599         self.failUnlessIn("Use 'tahoe ln' to duplicate a link", help)
600
601     def test_mkdir(self):
602         help = str(cli.MakeDirectoryOptions())
603         self.failUnlessIn("[options] [REMOTE_DIR]", help)
604         self.failUnlessIn("Create a new directory", help)
605
606     def test_backup(self):
607         help = str(cli.BackupOptions())
608         self.failUnlessIn("[options] FROM ALIAS:TO", help)
609
610     def test_webopen(self):
611         help = str(cli.WebopenOptions())
612         self.failUnlessIn("[options] [ALIAS:PATH]", help)
613
614     def test_manifest(self):
615         help = str(cli.ManifestOptions())
616         self.failUnlessIn("[options] [ALIAS:PATH]", help)
617
618     def test_stats(self):
619         help = str(cli.StatsOptions())
620         self.failUnlessIn("[options] [ALIAS:PATH]", help)
621
622     def test_check(self):
623         help = str(cli.CheckOptions())
624         self.failUnlessIn("[options] [ALIAS:PATH]", help)
625
626     def test_deep_check(self):
627         help = str(cli.DeepCheckOptions())
628         self.failUnlessIn("[options] [ALIAS:PATH]", help)
629
630     def test_create_alias(self):
631         help = str(cli.CreateAliasOptions())
632         self.failUnlessIn("[options] ALIAS[:]", help)
633
634     def test_add_alias(self):
635         help = str(cli.AddAliasOptions())
636         self.failUnlessIn("[options] ALIAS[:] DIRCAP", help)
637
638     def test_list_aliases(self):
639         help = str(cli.ListAliasesOptions())
640         self.failUnlessIn("[options]", help)
641
642     def test_start(self):
643         help = str(startstop_node.StartOptions())
644         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
645
646     def test_stop(self):
647         help = str(startstop_node.StopOptions())
648         self.failUnlessIn("[options] [NODEDIR]", help)
649
650     def test_restart(self):
651         help = str(startstop_node.RestartOptions())
652         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
653
654     def test_run(self):
655         help = str(startstop_node.RunOptions())
656         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
657
658     def test_create_client(self):
659         help = str(create_node.CreateClientOptions())
660         self.failUnlessIn("[options] [NODEDIR]", help)
661
662     def test_create_node(self):
663         help = str(create_node.CreateNodeOptions())
664         self.failUnlessIn("[options] [NODEDIR]", help)
665
666     def test_create_introducer(self):
667         help = str(create_node.CreateIntroducerOptions())
668         self.failUnlessIn("[options] NODEDIR", help)
669
670     def test_debug_trial(self):
671         help = str(debug.TrialOptions())
672         self.failUnlessIn(" [global-options] debug trial [options] [[file|package|module|TestCase|testmethod]...]", help)
673         self.failUnlessIn("The 'tahoe debug trial' command uses the correct imports", help)
674
675     def test_debug_flogtool(self):
676         options = debug.FlogtoolOptions()
677         help = str(options)
678         self.failUnlessIn(" [global-options] debug flogtool ", help)
679         self.failUnlessIn("The 'tahoe debug flogtool' command uses the correct imports", help)
680
681         for (option, shortcut, oClass, desc) in options.subCommands:
682             subhelp = str(oClass())
683             self.failUnlessIn(" [global-options] debug flogtool %s " % (option,), subhelp)
684
685
686 class Ln(GridTestMixin, CLITestMixin, unittest.TestCase):
687     def _create_test_file(self):
688         data = "puppies" * 1000
689         path = os.path.join(self.basedir, "datafile")
690         fileutil.write(path, data)
691         self.datafile = path
692
693     def test_ln_without_alias(self):
694         # if invoked without an alias when the 'tahoe' alias doesn't
695         # exist, 'tahoe ln' should output a useful error message and not
696         # a stack trace
697         self.basedir = "cli/Ln/ln_without_alias"
698         self.set_up_grid()
699         d = self.do_cli("ln", "from", "to")
700         def _check((rc, out, err)):
701             self.failUnlessReallyEqual(rc, 1)
702             self.failUnlessIn("error:", err)
703             self.failUnlessReallyEqual(out, "")
704         d.addCallback(_check)
705         # Make sure that validation extends to the "to" parameter
706         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
707         d.addCallback(lambda ign: self._create_test_file())
708         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
709                                               "havasu:from"))
710         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "to"))
711         d.addCallback(_check)
712         return d
713
714     def test_ln_with_nonexistent_alias(self):
715         # If invoked with aliases that don't exist, 'tahoe ln' should
716         # output a useful error message and not a stack trace.
717         self.basedir = "cli/Ln/ln_with_nonexistent_alias"
718         self.set_up_grid()
719         d = self.do_cli("ln", "havasu:from", "havasu:to")
720         def _check((rc, out, err)):
721             self.failUnlessReallyEqual(rc, 1)
722             self.failUnlessIn("error:", err)
723         d.addCallback(_check)
724         # Make sure that validation occurs on the to parameter if the
725         # from parameter passes.
726         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
727         d.addCallback(lambda ign: self._create_test_file())
728         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
729                                               "havasu:from"))
730         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "huron:to"))
731         d.addCallback(_check)
732         return d
733
734
735 class Admin(unittest.TestCase):
736     def do_cli(self, *args, **kwargs):
737         argv = list(args)
738         stdin = kwargs.get("stdin", "")
739         stdout, stderr = StringIO(), StringIO()
740         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
741                                   stdin=StringIO(stdin),
742                                   stdout=stdout, stderr=stderr)
743         def _done(res):
744             return stdout.getvalue(), stderr.getvalue()
745         d.addCallback(_done)
746         return d
747
748     def test_generate_keypair(self):
749         d = self.do_cli("admin", "generate-keypair")
750         def _done( (stdout, stderr) ):
751             lines = [line.strip() for line in stdout.splitlines()]
752             privkey_bits = lines[0].split()
753             pubkey_bits = lines[1].split()
754             sk_header = "private:"
755             vk_header = "public:"
756             self.failUnlessEqual(privkey_bits[0], sk_header, lines[0])
757             self.failUnlessEqual(pubkey_bits[0], vk_header, lines[1])
758             self.failUnless(privkey_bits[1].startswith("priv-v0-"), lines[0])
759             self.failUnless(pubkey_bits[1].startswith("pub-v0-"), lines[1])
760             sk_bytes = base32.a2b(keyutil.remove_prefix(privkey_bits[1], "priv-v0-"))
761             sk = ed25519.SigningKey(sk_bytes)
762             vk_bytes = base32.a2b(keyutil.remove_prefix(pubkey_bits[1], "pub-v0-"))
763             self.failUnlessEqual(sk.get_verifying_key_bytes(), vk_bytes)
764         d.addCallback(_done)
765         return d
766
767     def test_derive_pubkey(self):
768         priv1,pub1 = keyutil.make_keypair()
769         d = self.do_cli("admin", "derive-pubkey", priv1)
770         def _done( (stdout, stderr) ):
771             lines = stdout.split("\n")
772             privkey_line = lines[0].strip()
773             pubkey_line = lines[1].strip()
774             sk_header = "private: priv-v0-"
775             vk_header = "public: pub-v0-"
776             self.failUnless(privkey_line.startswith(sk_header), privkey_line)
777             self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
778             pub2 = pubkey_line[len(vk_header):]
779             self.failUnlessEqual("pub-v0-"+pub2, pub1)
780         d.addCallback(_done)
781         return d
782
783
784 class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
785     def test_get(self):
786         self.basedir = "cli/Errors/get"
787         self.set_up_grid()
788         c0 = self.g.clients[0]
789         self.fileurls = {}
790         DATA = "data" * 100
791         d = c0.upload(upload.Data(DATA, convergence=""))
792         def _stash_bad(ur):
793             self.uri_1share = ur.get_uri()
794             self.delete_shares_numbered(ur.get_uri(), range(1,10))
795         d.addCallback(_stash_bad)
796
797         # the download is abandoned as soon as it's clear that we won't get
798         # enough shares. The one remaining share might be in either the
799         # COMPLETE or the PENDING state.
800         in_complete_msg = "ran out of shares: complete=sh0 pending= overdue= unused= need 3"
801         in_pending_msg = "ran out of shares: complete= pending=Share(sh0-on-fob7vqgd) overdue= unused= need 3"
802
803         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share))
804         def _check1((rc, out, err)):
805             self.failIfEqual(rc, 0)
806             self.failUnless("410 Gone" in err, err)
807             self.failUnlessIn("NotEnoughSharesError: ", err)
808             self.failUnless(in_complete_msg in err or in_pending_msg in err,
809                             err)
810         d.addCallback(_check1)
811
812         targetf = os.path.join(self.basedir, "output")
813         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share, targetf))
814         def _check2((rc, out, err)):
815             self.failIfEqual(rc, 0)
816             self.failUnless("410 Gone" in err, err)
817             self.failUnlessIn("NotEnoughSharesError: ", err)
818             self.failUnless(in_complete_msg in err or in_pending_msg in err,
819                             err)
820             self.failIf(os.path.exists(targetf))
821         d.addCallback(_check2)
822
823         return d
824
825     def test_broken_socket(self):
826         # When the http connection breaks (such as when node.url is overwritten
827         # by a confused user), a user friendly error message should be printed.
828         self.basedir = "cli/Errors/test_broken_socket"
829         self.set_up_grid()
830
831         # Simulate a connection error
832         def _socket_error(*args, **kwargs):
833             raise socket_error('test error')
834         self.patch(allmydata.scripts.common_http.httplib.HTTPConnection,
835                    "endheaders", _socket_error)
836
837         d = self.do_cli("mkdir")
838         def _check_invalid((rc,stdout,stderr)):
839             self.failIfEqual(rc, 0)
840             self.failUnlessIn("Error trying to connect to http://127.0.0.1", stderr)
841         d.addCallback(_check_invalid)
842         return d
843
844
845 class Get(GridTestMixin, CLITestMixin, unittest.TestCase):
846     def test_get_without_alias(self):
847         # 'tahoe get' should output a useful error message when invoked
848         # without an explicit alias and when the default 'tahoe' alias
849         # hasn't been created yet.
850         self.basedir = "cli/Get/get_without_alias"
851         self.set_up_grid()
852         d = self.do_cli('get', 'file')
853         def _check((rc, out, err)):
854             self.failUnlessReallyEqual(rc, 1)
855             self.failUnlessIn("error:", err)
856             self.failUnlessReallyEqual(out, "")
857         d.addCallback(_check)
858         return d
859
860     def test_get_with_nonexistent_alias(self):
861         # 'tahoe get' should output a useful error message when invoked with
862         # an explicit alias that doesn't exist.
863         self.basedir = "cli/Get/get_with_nonexistent_alias"
864         self.set_up_grid()
865         d = self.do_cli("get", "nonexistent:file")
866         def _check((rc, out, err)):
867             self.failUnlessReallyEqual(rc, 1)
868             self.failUnlessIn("error:", err)
869             self.failUnlessIn("nonexistent", err)
870             self.failUnlessReallyEqual(out, "")
871         d.addCallback(_check)
872         return d
873
874
875 class Manifest(GridTestMixin, CLITestMixin, unittest.TestCase):
876     def test_manifest_without_alias(self):
877         # 'tahoe manifest' should output a useful error message when invoked
878         # without an explicit alias when the default 'tahoe' alias is
879         # missing.
880         self.basedir = "cli/Manifest/manifest_without_alias"
881         self.set_up_grid()
882         d = self.do_cli("manifest")
883         def _check((rc, out, err)):
884             self.failUnlessReallyEqual(rc, 1)
885             self.failUnlessIn("error:", err)
886             self.failUnlessReallyEqual(out, "")
887         d.addCallback(_check)
888         return d
889
890     def test_manifest_with_nonexistent_alias(self):
891         # 'tahoe manifest' should output a useful error message when invoked
892         # with an explicit alias that doesn't exist.
893         self.basedir = "cli/Manifest/manifest_with_nonexistent_alias"
894         self.set_up_grid()
895         d = self.do_cli("manifest", "nonexistent:")
896         def _check((rc, out, err)):
897             self.failUnlessReallyEqual(rc, 1)
898             self.failUnlessIn("error:", err)
899             self.failUnlessIn("nonexistent", err)
900             self.failUnlessReallyEqual(out, "")
901         d.addCallback(_check)
902         return d
903
904
905 class Mkdir(GridTestMixin, CLITestMixin, unittest.TestCase):
906     def test_mkdir(self):
907         self.basedir = os.path.dirname(self.mktemp())
908         self.set_up_grid()
909
910         d = self.do_cli("create-alias", "tahoe")
911         d.addCallback(lambda res: self.do_cli("mkdir", "test"))
912         def _check((rc, out, err)):
913             self.failUnlessReallyEqual(rc, 0)
914             self.failUnlessReallyEqual(err, "")
915             self.failUnlessIn("URI:", out)
916         d.addCallback(_check)
917
918         return d
919
920     def test_mkdir_mutable_type(self):
921         self.basedir = os.path.dirname(self.mktemp())
922         self.set_up_grid()
923         d = self.do_cli("create-alias", "tahoe")
924         def _check((rc, out, err), st):
925             self.failUnlessReallyEqual(rc, 0)
926             self.failUnlessReallyEqual(err, "")
927             self.failUnlessIn(st, out)
928             return out
929         def _mkdir(ign, mutable_type, uri_prefix, dirname):
930             d2 = self.do_cli("mkdir", "--format="+mutable_type, dirname)
931             d2.addCallback(_check, uri_prefix)
932             def _stash_filecap(cap):
933                 u = uri.from_string(cap)
934                 fn_uri = u.get_filenode_cap()
935                 self._filecap = fn_uri.to_string()
936             d2.addCallback(_stash_filecap)
937             d2.addCallback(lambda ign: self.do_cli("ls", "--json", dirname))
938             d2.addCallback(_check, uri_prefix)
939             d2.addCallback(lambda ign: self.do_cli("ls", "--json", self._filecap))
940             d2.addCallback(_check, '"format": "%s"' % (mutable_type.upper(),))
941             return d2
942
943         d.addCallback(_mkdir, "sdmf", "URI:DIR2", "tahoe:foo")
944         d.addCallback(_mkdir, "SDMF", "URI:DIR2", "tahoe:foo2")
945         d.addCallback(_mkdir, "mdmf", "URI:DIR2-MDMF", "tahoe:bar")
946         d.addCallback(_mkdir, "MDMF", "URI:DIR2-MDMF", "tahoe:bar2")
947         return d
948
949     def test_mkdir_mutable_type_unlinked(self):
950         self.basedir = os.path.dirname(self.mktemp())
951         self.set_up_grid()
952         d = self.do_cli("mkdir", "--format=SDMF")
953         def _check((rc, out, err), st):
954             self.failUnlessReallyEqual(rc, 0)
955             self.failUnlessReallyEqual(err, "")
956             self.failUnlessIn(st, out)
957             return out
958         d.addCallback(_check, "URI:DIR2")
959         def _stash_dircap(cap):
960             self._dircap = cap
961             # Now we're going to feed the cap into uri.from_string...
962             u = uri.from_string(cap)
963             # ...grab the underlying filenode uri.
964             fn_uri = u.get_filenode_cap()
965             # ...and stash that.
966             self._filecap = fn_uri.to_string()
967         d.addCallback(_stash_dircap)
968         d.addCallback(lambda res: self.do_cli("ls", "--json",
969                                               self._filecap))
970         d.addCallback(_check, '"format": "SDMF"')
971         d.addCallback(lambda res: self.do_cli("mkdir", "--format=MDMF"))
972         d.addCallback(_check, "URI:DIR2-MDMF")
973         d.addCallback(_stash_dircap)
974         d.addCallback(lambda res: self.do_cli("ls", "--json",
975                                               self._filecap))
976         d.addCallback(_check, '"format": "MDMF"')
977         return d
978
979     def test_mkdir_bad_mutable_type(self):
980         o = cli.MakeDirectoryOptions()
981         self.failUnlessRaises(usage.UsageError,
982                               o.parseOptions,
983                               ["--format=LDMF"])
984
985     def test_mkdir_unicode(self):
986         self.basedir = os.path.dirname(self.mktemp())
987         self.set_up_grid()
988
989         try:
990             motorhead_arg = u"tahoe:Mot\u00F6rhead".encode(get_io_encoding())
991         except UnicodeEncodeError:
992             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
993
994         d = self.do_cli("create-alias", "tahoe")
995         d.addCallback(lambda res: self.do_cli("mkdir", motorhead_arg))
996         def _check((rc, out, err)):
997             self.failUnlessReallyEqual(rc, 0)
998             self.failUnlessReallyEqual(err, "")
999             self.failUnlessIn("URI:", out)
1000         d.addCallback(_check)
1001
1002         return d
1003
1004     def test_mkdir_with_nonexistent_alias(self):
1005         # when invoked with an alias that doesn't exist, 'tahoe mkdir' should
1006         # output a sensible error message rather than a stack trace.
1007         self.basedir = "cli/Mkdir/mkdir_with_nonexistent_alias"
1008         self.set_up_grid()
1009         d = self.do_cli("mkdir", "havasu:")
1010         def _check((rc, out, err)):
1011             self.failUnlessReallyEqual(rc, 1)
1012             self.failUnlessIn("error:", err)
1013             self.failUnlessReallyEqual(out, "")
1014         d.addCallback(_check)
1015         return d
1016
1017
1018 class Unlink(GridTestMixin, CLITestMixin, unittest.TestCase):
1019     command = "unlink"
1020
1021     def _create_test_file(self):
1022         data = "puppies" * 1000
1023         path = os.path.join(self.basedir, "datafile")
1024         fileutil.write(path, data)
1025         self.datafile = path
1026
1027     def test_unlink_without_alias(self):
1028         # 'tahoe unlink' should behave sensibly when invoked without an explicit
1029         # alias before the default 'tahoe' alias has been created.
1030         self.basedir = "cli/Unlink/%s_without_alias" % (self.command,)
1031         self.set_up_grid()
1032         d = self.do_cli(self.command, "afile")
1033         def _check((rc, out, err)):
1034             self.failUnlessReallyEqual(rc, 1)
1035             self.failUnlessIn("error:", err)
1036             self.failUnlessReallyEqual(out, "")
1037         d.addCallback(_check)
1038
1039         d.addCallback(lambda ign: self.do_cli(self.command, "afile"))
1040         d.addCallback(_check)
1041         return d
1042
1043     def test_unlink_with_nonexistent_alias(self):
1044         # 'tahoe unlink' should behave sensibly when invoked with an explicit
1045         # alias that doesn't exist.
1046         self.basedir = "cli/Unlink/%s_with_nonexistent_alias" % (self.command,)
1047         self.set_up_grid()
1048         d = self.do_cli(self.command, "nonexistent:afile")
1049         def _check((rc, out, err)):
1050             self.failUnlessReallyEqual(rc, 1)
1051             self.failUnlessIn("error:", err)
1052             self.failUnlessIn("nonexistent", err)
1053             self.failUnlessReallyEqual(out, "")
1054         d.addCallback(_check)
1055
1056         d.addCallback(lambda ign: self.do_cli(self.command, "nonexistent:afile"))
1057         d.addCallback(_check)
1058         return d
1059
1060     def test_unlink_without_path(self):
1061         # 'tahoe unlink' should give a sensible error message when invoked without a path.
1062         self.basedir = "cli/Unlink/%s_without_path" % (self.command,)
1063         self.set_up_grid()
1064         self._create_test_file()
1065         d = self.do_cli("create-alias", "tahoe")
1066         d.addCallback(lambda ign: self.do_cli("put", self.datafile, "tahoe:test"))
1067         def _do_unlink((rc, out, err)):
1068             self.failUnlessReallyEqual(rc, 0)
1069             self.failUnless(out.startswith("URI:"), out)
1070             return self.do_cli(self.command, out.strip('\n'))
1071         d.addCallback(_do_unlink)
1072
1073         def _check((rc, out, err)):
1074             self.failUnlessReallyEqual(rc, 1)
1075             self.failUnlessIn("'tahoe %s'" % (self.command,), err)
1076             self.failUnlessIn("path must be given", err)
1077             self.failUnlessReallyEqual(out, "")
1078         d.addCallback(_check)
1079         return d
1080
1081
1082 class Rm(Unlink):
1083     """Test that 'tahoe rm' behaves in the same way as 'tahoe unlink'."""
1084     command = "rm"
1085
1086
1087 class Stats(GridTestMixin, CLITestMixin, unittest.TestCase):
1088     def test_empty_directory(self):
1089         self.basedir = "cli/Stats/empty_directory"
1090         self.set_up_grid()
1091         c0 = self.g.clients[0]
1092         self.fileurls = {}
1093         d = c0.create_dirnode()
1094         def _stash_root(n):
1095             self.rootnode = n
1096             self.rooturi = n.get_uri()
1097         d.addCallback(_stash_root)
1098
1099         # make sure we can get stats on an empty directory too
1100         d.addCallback(lambda ign: self.do_cli("stats", self.rooturi))
1101         def _check_stats((rc, out, err)):
1102             self.failUnlessReallyEqual(err, "")
1103             self.failUnlessReallyEqual(rc, 0)
1104             lines = out.splitlines()
1105             self.failUnlessIn(" count-immutable-files: 0", lines)
1106             self.failUnlessIn("   count-mutable-files: 0", lines)
1107             self.failUnlessIn("   count-literal-files: 0", lines)
1108             self.failUnlessIn("     count-directories: 1", lines)
1109             self.failUnlessIn("  size-immutable-files: 0", lines)
1110             self.failIfIn("Size Histogram:", lines)
1111         d.addCallback(_check_stats)
1112
1113         return d
1114
1115     def test_stats_without_alias(self):
1116         # when invoked with no explicit alias and before the default 'tahoe'
1117         # alias is created, 'tahoe stats' should output an informative error
1118         # message, not a stack trace.
1119         self.basedir = "cli/Stats/stats_without_alias"
1120         self.set_up_grid()
1121         d = self.do_cli("stats")
1122         def _check((rc, out, err)):
1123             self.failUnlessReallyEqual(rc, 1)
1124             self.failUnlessIn("error:", err)
1125             self.failUnlessReallyEqual(out, "")
1126         d.addCallback(_check)
1127         return d
1128
1129     def test_stats_with_nonexistent_alias(self):
1130         # when invoked with an explicit alias that doesn't exist,
1131         # 'tahoe stats' should output a useful error message.
1132         self.basedir = "cli/Stats/stats_with_nonexistent_alias"
1133         self.set_up_grid()
1134         d = self.do_cli("stats", "havasu:")
1135         def _check((rc, out, err)):
1136             self.failUnlessReallyEqual(rc, 1)
1137             self.failUnlessIn("error:", err)
1138             self.failUnlessReallyEqual(out, "")
1139         d.addCallback(_check)
1140         return d
1141
1142
1143 class Webopen(GridTestMixin, CLITestMixin, unittest.TestCase):
1144     def test_webopen_with_nonexistent_alias(self):
1145         # when invoked with an alias that doesn't exist, 'tahoe webopen'
1146         # should output an informative error message instead of a stack
1147         # trace.
1148         self.basedir = "cli/Webopen/webopen_with_nonexistent_alias"
1149         self.set_up_grid()
1150         d = self.do_cli("webopen", "fake:")
1151         def _check((rc, out, err)):
1152             self.failUnlessReallyEqual(rc, 1)
1153             self.failUnlessIn("error:", err)
1154             self.failUnlessReallyEqual(out, "")
1155         d.addCallback(_check)
1156         return d
1157
1158     def test_webopen(self):
1159         # TODO: replace with @patch that supports Deferreds.
1160         import webbrowser
1161         def call_webbrowser_open(url):
1162             self.failUnlessIn(self.alias_uri.replace(':', '%3A'), url)
1163             self.webbrowser_open_called = True
1164         def _cleanup(res):
1165             webbrowser.open = self.old_webbrowser_open
1166             return res
1167
1168         self.old_webbrowser_open = webbrowser.open
1169         try:
1170             webbrowser.open = call_webbrowser_open
1171
1172             self.basedir = "cli/Webopen/webopen"
1173             self.set_up_grid()
1174             d = self.do_cli("create-alias", "alias:")
1175             def _check_alias((rc, out, err)):
1176                 self.failUnlessReallyEqual(rc, 0, repr((rc, out, err)))
1177                 self.failUnlessIn("Alias 'alias' created", out)
1178                 self.failUnlessReallyEqual(err, "")
1179                 self.alias_uri = get_aliases(self.get_clientdir())["alias"]
1180             d.addCallback(_check_alias)
1181             d.addCallback(lambda res: self.do_cli("webopen", "alias:"))
1182             def _check_webopen((rc, out, err)):
1183                 self.failUnlessReallyEqual(rc, 0, repr((rc, out, err)))
1184                 self.failUnlessReallyEqual(out, "")
1185                 self.failUnlessReallyEqual(err, "")
1186                 self.failUnless(self.webbrowser_open_called)
1187             d.addCallback(_check_webopen)
1188             d.addBoth(_cleanup)
1189         except:
1190             _cleanup(None)
1191             raise
1192         return d
1193
1194 class Options(ReallyEqualMixin, unittest.TestCase):
1195     # this test case only looks at argument-processing and simple stuff.
1196
1197     def parse(self, args, stdout=None):
1198         o = runner.Options()
1199         if stdout is not None:
1200             o.stdout = stdout
1201         o.parseOptions(args)
1202         while hasattr(o, "subOptions"):
1203             o = o.subOptions
1204         return o
1205
1206     def test_list(self):
1207         fileutil.rm_dir("cli/test_options")
1208         fileutil.make_dirs("cli/test_options")
1209         fileutil.make_dirs("cli/test_options/private")
1210         fileutil.write("cli/test_options/node.url", "http://localhost:8080/\n")
1211         filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
1212                                                fingerprint="\x00"*32)
1213         private_uri = uri.DirectoryURI(filenode_uri).to_string()
1214         fileutil.write("cli/test_options/private/root_dir.cap", private_uri + "\n")
1215         def parse2(args): return parse_options("cli/test_options", "ls", args)
1216         o = parse2([])
1217         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1218         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
1219         self.failUnlessEqual(o.where, u"")
1220
1221         o = parse2(["--node-url", "http://example.org:8111/"])
1222         self.failUnlessEqual(o['node-url'], "http://example.org:8111/")
1223         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
1224         self.failUnlessEqual(o.where, u"")
1225
1226         o = parse2(["--dir-cap", "root"])
1227         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1228         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], "root")
1229         self.failUnlessEqual(o.where, u"")
1230
1231         other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
1232                                                      fingerprint="\x11"*32)
1233         other_uri = uri.DirectoryURI(other_filenode_uri).to_string()
1234         o = parse2(["--dir-cap", other_uri])
1235         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1236         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
1237         self.failUnlessEqual(o.where, u"")
1238
1239         o = parse2(["--dir-cap", other_uri, "subdir"])
1240         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1241         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
1242         self.failUnlessEqual(o.where, u"subdir")
1243
1244         self.failUnlessRaises(usage.UsageError, parse2,
1245                               ["--node-url", "NOT-A-URL"])
1246
1247         o = parse2(["--node-url", "http://localhost:8080"])
1248         self.failUnlessEqual(o["node-url"], "http://localhost:8080/")
1249
1250         o = parse2(["--node-url", "https://localhost/"])
1251         self.failUnlessEqual(o["node-url"], "https://localhost/")
1252
1253     def test_version(self):
1254         # "tahoe --version" dumps text to stdout and exits
1255         stdout = StringIO()
1256         self.failUnlessRaises(SystemExit, self.parse, ["--version"], stdout)
1257         self.failUnlessIn(allmydata.__appname__ + ":", stdout.getvalue())
1258         # but "tahoe SUBCOMMAND --version" should be rejected
1259         self.failUnlessRaises(usage.UsageError, self.parse,
1260                               ["start", "--version"])
1261         self.failUnlessRaises(usage.UsageError, self.parse,
1262                               ["start", "--version-and-path"])
1263
1264     def test_quiet(self):
1265         # accepted as an overall option, but not on subcommands
1266         o = self.parse(["--quiet", "start"])
1267         self.failUnless(o.parent["quiet"])
1268         self.failUnlessRaises(usage.UsageError, self.parse,
1269                               ["start", "--quiet"])
1270
1271     def test_basedir(self):
1272         # accept a --node-directory option before the verb, or a --basedir
1273         # option after, or a basedir argument after, but none in the wrong
1274         # place, and not more than one of the three.
1275         o = self.parse(["start"])
1276         self.failUnlessReallyEqual(o["basedir"], os.path.join(fileutil.abspath_expanduser_unicode(u"~"),
1277                                                               u".tahoe"))
1278         o = self.parse(["start", "here"])
1279         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"here"))
1280         o = self.parse(["start", "--basedir", "there"])
1281         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"there"))
1282         o = self.parse(["--node-directory", "there", "start"])
1283         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"there"))
1284
1285         o = self.parse(["start", "here", "--nodaemon"])
1286         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"here"))
1287
1288         self.failUnlessRaises(usage.UsageError, self.parse,
1289                               ["--basedir", "there", "start"])
1290         self.failUnlessRaises(usage.UsageError, self.parse,
1291                               ["start", "--node-directory", "there"])
1292
1293         self.failUnlessRaises(usage.UsageError, self.parse,
1294                               ["--node-directory=there",
1295                                "start", "--basedir=here"])
1296         self.failUnlessRaises(usage.UsageError, self.parse,
1297                               ["start", "--basedir=here", "anywhere"])
1298         self.failUnlessRaises(usage.UsageError, self.parse,
1299                               ["--node-directory=there",
1300                                "start", "anywhere"])
1301         self.failUnlessRaises(usage.UsageError, self.parse,
1302                               ["--node-directory=there",
1303                                "start", "--basedir=here", "anywhere"])
1304
1305         self.failUnlessRaises(usage.UsageError, self.parse,
1306                               ["--node-directory=there", "start", "--nodaemon"])
1307         self.failUnlessRaises(usage.UsageError, self.parse,
1308                               ["start", "--basedir=here", "--nodaemon"])