]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_cli.py
5d39814924c9c0969e88df0ad6942abc2d4f4308
[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 failUnlessInNormalized(self, x, y):
565         # helper function to deal with the --help output being wrapped to
566         # various widths, depending on the $COLUMNS environment variable
567         self.failUnlessIn(x.replace("\n", " "), y.replace("\n", " "))
568
569     def test_get(self):
570         help = str(cli.GetOptions())
571         self.failUnlessIn("[options] REMOTE_FILE LOCAL_FILE", help)
572         self.failUnlessIn("% tahoe get FOO |less", help)
573
574     def test_put(self):
575         help = str(cli.PutOptions())
576         self.failUnlessIn("[options] LOCAL_FILE REMOTE_FILE", help)
577         self.failUnlessIn("% cat FILE | tahoe put", help)
578
579     def test_ls(self):
580         help = str(cli.ListOptions())
581         self.failUnlessIn("[options] [PATH]", help)
582
583     def test_unlink(self):
584         help = str(cli.UnlinkOptions())
585         self.failUnlessIn("[options] REMOTE_FILE", help)
586
587     def test_rm(self):
588         help = str(cli.RmOptions())
589         self.failUnlessIn("[options] REMOTE_FILE", help)
590
591     def test_mv(self):
592         help = str(cli.MvOptions())
593         self.failUnlessIn("[options] FROM TO", help)
594         self.failUnlessInNormalized("Use 'tahoe mv' to move files", help)
595
596     def test_cp(self):
597         help = str(cli.CpOptions())
598         self.failUnlessIn("[options] FROM.. TO", help)
599         self.failUnlessInNormalized("Use 'tahoe cp' to copy files", help)
600
601     def test_ln(self):
602         help = str(cli.LnOptions())
603         self.failUnlessIn("[options] FROM_LINK TO_LINK", help)
604         self.failUnlessInNormalized("Use 'tahoe ln' to duplicate a link", help)
605
606     def test_mkdir(self):
607         help = str(cli.MakeDirectoryOptions())
608         self.failUnlessIn("[options] [REMOTE_DIR]", help)
609         self.failUnlessInNormalized("Create a new directory", help)
610
611     def test_backup(self):
612         help = str(cli.BackupOptions())
613         self.failUnlessIn("[options] FROM ALIAS:TO", help)
614
615     def test_webopen(self):
616         help = str(cli.WebopenOptions())
617         self.failUnlessIn("[options] [ALIAS:PATH]", help)
618
619     def test_manifest(self):
620         help = str(cli.ManifestOptions())
621         self.failUnlessIn("[options] [ALIAS:PATH]", help)
622
623     def test_stats(self):
624         help = str(cli.StatsOptions())
625         self.failUnlessIn("[options] [ALIAS:PATH]", help)
626
627     def test_check(self):
628         help = str(cli.CheckOptions())
629         self.failUnlessIn("[options] [ALIAS:PATH]", help)
630
631     def test_deep_check(self):
632         help = str(cli.DeepCheckOptions())
633         self.failUnlessIn("[options] [ALIAS:PATH]", help)
634
635     def test_create_alias(self):
636         help = str(cli.CreateAliasOptions())
637         self.failUnlessIn("[options] ALIAS[:]", help)
638
639     def test_add_alias(self):
640         help = str(cli.AddAliasOptions())
641         self.failUnlessIn("[options] ALIAS[:] DIRCAP", help)
642
643     def test_list_aliases(self):
644         help = str(cli.ListAliasesOptions())
645         self.failUnlessIn("[options]", help)
646
647     def test_start(self):
648         help = str(startstop_node.StartOptions())
649         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
650
651     def test_stop(self):
652         help = str(startstop_node.StopOptions())
653         self.failUnlessIn("[options] [NODEDIR]", help)
654
655     def test_restart(self):
656         help = str(startstop_node.RestartOptions())
657         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
658
659     def test_run(self):
660         help = str(startstop_node.RunOptions())
661         self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
662
663     def test_create_client(self):
664         help = str(create_node.CreateClientOptions())
665         self.failUnlessIn("[options] [NODEDIR]", help)
666
667     def test_create_node(self):
668         help = str(create_node.CreateNodeOptions())
669         self.failUnlessIn("[options] [NODEDIR]", help)
670
671     def test_create_introducer(self):
672         help = str(create_node.CreateIntroducerOptions())
673         self.failUnlessIn("[options] NODEDIR", help)
674
675     def test_debug_trial(self):
676         help = str(debug.TrialOptions())
677         self.failUnlessIn(" [global-options] debug trial [options] [[file|package|module|TestCase|testmethod]...]", help)
678         self.failUnlessInNormalized("The 'tahoe debug trial' command uses the correct imports", help)
679
680     def test_debug_flogtool(self):
681         options = debug.FlogtoolOptions()
682         help = str(options)
683         self.failUnlessIn(" [global-options] debug flogtool ", help)
684         self.failUnlessInNormalized("The 'tahoe debug flogtool' command uses the correct imports", help)
685
686         for (option, shortcut, oClass, desc) in options.subCommands:
687             subhelp = str(oClass())
688             self.failUnlessIn(" [global-options] debug flogtool %s " % (option,), subhelp)
689
690
691 class Ln(GridTestMixin, CLITestMixin, unittest.TestCase):
692     def _create_test_file(self):
693         data = "puppies" * 1000
694         path = os.path.join(self.basedir, "datafile")
695         fileutil.write(path, data)
696         self.datafile = path
697
698     def test_ln_without_alias(self):
699         # if invoked without an alias when the 'tahoe' alias doesn't
700         # exist, 'tahoe ln' should output a useful error message and not
701         # a stack trace
702         self.basedir = "cli/Ln/ln_without_alias"
703         self.set_up_grid()
704         d = self.do_cli("ln", "from", "to")
705         def _check((rc, out, err)):
706             self.failUnlessReallyEqual(rc, 1)
707             self.failUnlessIn("error:", err)
708             self.failUnlessReallyEqual(out, "")
709         d.addCallback(_check)
710         # Make sure that validation extends to the "to" parameter
711         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
712         d.addCallback(lambda ign: self._create_test_file())
713         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
714                                               "havasu:from"))
715         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "to"))
716         d.addCallback(_check)
717         return d
718
719     def test_ln_with_nonexistent_alias(self):
720         # If invoked with aliases that don't exist, 'tahoe ln' should
721         # output a useful error message and not a stack trace.
722         self.basedir = "cli/Ln/ln_with_nonexistent_alias"
723         self.set_up_grid()
724         d = self.do_cli("ln", "havasu:from", "havasu:to")
725         def _check((rc, out, err)):
726             self.failUnlessReallyEqual(rc, 1)
727             self.failUnlessIn("error:", err)
728         d.addCallback(_check)
729         # Make sure that validation occurs on the to parameter if the
730         # from parameter passes.
731         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
732         d.addCallback(lambda ign: self._create_test_file())
733         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
734                                               "havasu:from"))
735         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "huron:to"))
736         d.addCallback(_check)
737         return d
738
739
740 class Admin(unittest.TestCase):
741     def do_cli(self, *args, **kwargs):
742         argv = list(args)
743         stdin = kwargs.get("stdin", "")
744         stdout, stderr = StringIO(), StringIO()
745         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
746                                   stdin=StringIO(stdin),
747                                   stdout=stdout, stderr=stderr)
748         def _done(res):
749             return stdout.getvalue(), stderr.getvalue()
750         d.addCallback(_done)
751         return d
752
753     def test_generate_keypair(self):
754         d = self.do_cli("admin", "generate-keypair")
755         def _done( (stdout, stderr) ):
756             lines = [line.strip() for line in stdout.splitlines()]
757             privkey_bits = lines[0].split()
758             pubkey_bits = lines[1].split()
759             sk_header = "private:"
760             vk_header = "public:"
761             self.failUnlessEqual(privkey_bits[0], sk_header, lines[0])
762             self.failUnlessEqual(pubkey_bits[0], vk_header, lines[1])
763             self.failUnless(privkey_bits[1].startswith("priv-v0-"), lines[0])
764             self.failUnless(pubkey_bits[1].startswith("pub-v0-"), lines[1])
765             sk_bytes = base32.a2b(keyutil.remove_prefix(privkey_bits[1], "priv-v0-"))
766             sk = ed25519.SigningKey(sk_bytes)
767             vk_bytes = base32.a2b(keyutil.remove_prefix(pubkey_bits[1], "pub-v0-"))
768             self.failUnlessEqual(sk.get_verifying_key_bytes(), vk_bytes)
769         d.addCallback(_done)
770         return d
771
772     def test_derive_pubkey(self):
773         priv1,pub1 = keyutil.make_keypair()
774         d = self.do_cli("admin", "derive-pubkey", priv1)
775         def _done( (stdout, stderr) ):
776             lines = stdout.split("\n")
777             privkey_line = lines[0].strip()
778             pubkey_line = lines[1].strip()
779             sk_header = "private: priv-v0-"
780             vk_header = "public: pub-v0-"
781             self.failUnless(privkey_line.startswith(sk_header), privkey_line)
782             self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
783             pub2 = pubkey_line[len(vk_header):]
784             self.failUnlessEqual("pub-v0-"+pub2, pub1)
785         d.addCallback(_done)
786         return d
787
788
789 class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
790     def test_get(self):
791         self.basedir = "cli/Errors/get"
792         self.set_up_grid()
793         c0 = self.g.clients[0]
794         self.fileurls = {}
795         DATA = "data" * 100
796         d = c0.upload(upload.Data(DATA, convergence=""))
797         def _stash_bad(ur):
798             self.uri_1share = ur.get_uri()
799             self.delete_shares_numbered(ur.get_uri(), range(1,10))
800         d.addCallback(_stash_bad)
801
802         # the download is abandoned as soon as it's clear that we won't get
803         # enough shares. The one remaining share might be in either the
804         # COMPLETE or the PENDING state.
805         in_complete_msg = "ran out of shares: complete=sh0 pending= overdue= unused= need 3"
806         in_pending_msg = "ran out of shares: complete= pending=Share(sh0-on-fob7vqgd) overdue= unused= need 3"
807
808         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share))
809         def _check1((rc, out, err)):
810             self.failIfEqual(rc, 0)
811             self.failUnless("410 Gone" in err, err)
812             self.failUnlessIn("NotEnoughSharesError: ", err)
813             self.failUnless(in_complete_msg in err or in_pending_msg in err,
814                             err)
815         d.addCallback(_check1)
816
817         targetf = os.path.join(self.basedir, "output")
818         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share, targetf))
819         def _check2((rc, out, err)):
820             self.failIfEqual(rc, 0)
821             self.failUnless("410 Gone" in err, err)
822             self.failUnlessIn("NotEnoughSharesError: ", err)
823             self.failUnless(in_complete_msg in err or in_pending_msg in err,
824                             err)
825             self.failIf(os.path.exists(targetf))
826         d.addCallback(_check2)
827
828         return d
829
830     def test_broken_socket(self):
831         # When the http connection breaks (such as when node.url is overwritten
832         # by a confused user), a user friendly error message should be printed.
833         self.basedir = "cli/Errors/test_broken_socket"
834         self.set_up_grid()
835
836         # Simulate a connection error
837         def _socket_error(*args, **kwargs):
838             raise socket_error('test error')
839         self.patch(allmydata.scripts.common_http.httplib.HTTPConnection,
840                    "endheaders", _socket_error)
841
842         d = self.do_cli("mkdir")
843         def _check_invalid((rc,stdout,stderr)):
844             self.failIfEqual(rc, 0)
845             self.failUnlessIn("Error trying to connect to http://127.0.0.1", stderr)
846         d.addCallback(_check_invalid)
847         return d
848
849
850 class Get(GridTestMixin, CLITestMixin, unittest.TestCase):
851     def test_get_without_alias(self):
852         # 'tahoe get' should output a useful error message when invoked
853         # without an explicit alias and when the default 'tahoe' alias
854         # hasn't been created yet.
855         self.basedir = "cli/Get/get_without_alias"
856         self.set_up_grid()
857         d = self.do_cli('get', 'file')
858         def _check((rc, out, err)):
859             self.failUnlessReallyEqual(rc, 1)
860             self.failUnlessIn("error:", err)
861             self.failUnlessReallyEqual(out, "")
862         d.addCallback(_check)
863         return d
864
865     def test_get_with_nonexistent_alias(self):
866         # 'tahoe get' should output a useful error message when invoked with
867         # an explicit alias that doesn't exist.
868         self.basedir = "cli/Get/get_with_nonexistent_alias"
869         self.set_up_grid()
870         d = self.do_cli("get", "nonexistent:file")
871         def _check((rc, out, err)):
872             self.failUnlessReallyEqual(rc, 1)
873             self.failUnlessIn("error:", err)
874             self.failUnlessIn("nonexistent", err)
875             self.failUnlessReallyEqual(out, "")
876         d.addCallback(_check)
877         return d
878
879
880 class Manifest(GridTestMixin, CLITestMixin, unittest.TestCase):
881     def test_manifest_without_alias(self):
882         # 'tahoe manifest' should output a useful error message when invoked
883         # without an explicit alias when the default 'tahoe' alias is
884         # missing.
885         self.basedir = "cli/Manifest/manifest_without_alias"
886         self.set_up_grid()
887         d = self.do_cli("manifest")
888         def _check((rc, out, err)):
889             self.failUnlessReallyEqual(rc, 1)
890             self.failUnlessIn("error:", err)
891             self.failUnlessReallyEqual(out, "")
892         d.addCallback(_check)
893         return d
894
895     def test_manifest_with_nonexistent_alias(self):
896         # 'tahoe manifest' should output a useful error message when invoked
897         # with an explicit alias that doesn't exist.
898         self.basedir = "cli/Manifest/manifest_with_nonexistent_alias"
899         self.set_up_grid()
900         d = self.do_cli("manifest", "nonexistent:")
901         def _check((rc, out, err)):
902             self.failUnlessReallyEqual(rc, 1)
903             self.failUnlessIn("error:", err)
904             self.failUnlessIn("nonexistent", err)
905             self.failUnlessReallyEqual(out, "")
906         d.addCallback(_check)
907         return d
908
909
910 class Mkdir(GridTestMixin, CLITestMixin, unittest.TestCase):
911     def test_mkdir(self):
912         self.basedir = os.path.dirname(self.mktemp())
913         self.set_up_grid()
914
915         d = self.do_cli("create-alias", "tahoe")
916         d.addCallback(lambda res: self.do_cli("mkdir", "test"))
917         def _check((rc, out, err)):
918             self.failUnlessReallyEqual(rc, 0)
919             self.failUnlessReallyEqual(err, "")
920             self.failUnlessIn("URI:", out)
921         d.addCallback(_check)
922
923         return d
924
925     def test_mkdir_mutable_type(self):
926         self.basedir = os.path.dirname(self.mktemp())
927         self.set_up_grid()
928         d = self.do_cli("create-alias", "tahoe")
929         def _check((rc, out, err), st):
930             self.failUnlessReallyEqual(rc, 0)
931             self.failUnlessReallyEqual(err, "")
932             self.failUnlessIn(st, out)
933             return out
934         def _mkdir(ign, mutable_type, uri_prefix, dirname):
935             d2 = self.do_cli("mkdir", "--format="+mutable_type, dirname)
936             d2.addCallback(_check, uri_prefix)
937             def _stash_filecap(cap):
938                 u = uri.from_string(cap)
939                 fn_uri = u.get_filenode_cap()
940                 self._filecap = fn_uri.to_string()
941             d2.addCallback(_stash_filecap)
942             d2.addCallback(lambda ign: self.do_cli("ls", "--json", dirname))
943             d2.addCallback(_check, uri_prefix)
944             d2.addCallback(lambda ign: self.do_cli("ls", "--json", self._filecap))
945             d2.addCallback(_check, '"format": "%s"' % (mutable_type.upper(),))
946             return d2
947
948         d.addCallback(_mkdir, "sdmf", "URI:DIR2", "tahoe:foo")
949         d.addCallback(_mkdir, "SDMF", "URI:DIR2", "tahoe:foo2")
950         d.addCallback(_mkdir, "mdmf", "URI:DIR2-MDMF", "tahoe:bar")
951         d.addCallback(_mkdir, "MDMF", "URI:DIR2-MDMF", "tahoe:bar2")
952         return d
953
954     def test_mkdir_mutable_type_unlinked(self):
955         self.basedir = os.path.dirname(self.mktemp())
956         self.set_up_grid()
957         d = self.do_cli("mkdir", "--format=SDMF")
958         def _check((rc, out, err), st):
959             self.failUnlessReallyEqual(rc, 0)
960             self.failUnlessReallyEqual(err, "")
961             self.failUnlessIn(st, out)
962             return out
963         d.addCallback(_check, "URI:DIR2")
964         def _stash_dircap(cap):
965             self._dircap = cap
966             # Now we're going to feed the cap into uri.from_string...
967             u = uri.from_string(cap)
968             # ...grab the underlying filenode uri.
969             fn_uri = u.get_filenode_cap()
970             # ...and stash that.
971             self._filecap = fn_uri.to_string()
972         d.addCallback(_stash_dircap)
973         d.addCallback(lambda res: self.do_cli("ls", "--json",
974                                               self._filecap))
975         d.addCallback(_check, '"format": "SDMF"')
976         d.addCallback(lambda res: self.do_cli("mkdir", "--format=MDMF"))
977         d.addCallback(_check, "URI:DIR2-MDMF")
978         d.addCallback(_stash_dircap)
979         d.addCallback(lambda res: self.do_cli("ls", "--json",
980                                               self._filecap))
981         d.addCallback(_check, '"format": "MDMF"')
982         return d
983
984     def test_mkdir_bad_mutable_type(self):
985         o = cli.MakeDirectoryOptions()
986         self.failUnlessRaises(usage.UsageError,
987                               o.parseOptions,
988                               ["--format=LDMF"])
989
990     def test_mkdir_unicode(self):
991         self.basedir = os.path.dirname(self.mktemp())
992         self.set_up_grid()
993
994         try:
995             motorhead_arg = u"tahoe:Mot\u00F6rhead".encode(get_io_encoding())
996         except UnicodeEncodeError:
997             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
998
999         d = self.do_cli("create-alias", "tahoe")
1000         d.addCallback(lambda res: self.do_cli("mkdir", motorhead_arg))
1001         def _check((rc, out, err)):
1002             self.failUnlessReallyEqual(rc, 0)
1003             self.failUnlessReallyEqual(err, "")
1004             self.failUnlessIn("URI:", out)
1005         d.addCallback(_check)
1006
1007         return d
1008
1009     def test_mkdir_with_nonexistent_alias(self):
1010         # when invoked with an alias that doesn't exist, 'tahoe mkdir' should
1011         # output a sensible error message rather than a stack trace.
1012         self.basedir = "cli/Mkdir/mkdir_with_nonexistent_alias"
1013         self.set_up_grid()
1014         d = self.do_cli("mkdir", "havasu:")
1015         def _check((rc, out, err)):
1016             self.failUnlessReallyEqual(rc, 1)
1017             self.failUnlessIn("error:", err)
1018             self.failUnlessReallyEqual(out, "")
1019         d.addCallback(_check)
1020         return d
1021
1022
1023 class Unlink(GridTestMixin, CLITestMixin, unittest.TestCase):
1024     command = "unlink"
1025
1026     def _create_test_file(self):
1027         data = "puppies" * 1000
1028         path = os.path.join(self.basedir, "datafile")
1029         fileutil.write(path, data)
1030         self.datafile = path
1031
1032     def test_unlink_without_alias(self):
1033         # 'tahoe unlink' should behave sensibly when invoked without an explicit
1034         # alias before the default 'tahoe' alias has been created.
1035         self.basedir = "cli/Unlink/%s_without_alias" % (self.command,)
1036         self.set_up_grid()
1037         d = self.do_cli(self.command, "afile")
1038         def _check((rc, out, err)):
1039             self.failUnlessReallyEqual(rc, 1)
1040             self.failUnlessIn("error:", err)
1041             self.failUnlessReallyEqual(out, "")
1042         d.addCallback(_check)
1043
1044         d.addCallback(lambda ign: self.do_cli(self.command, "afile"))
1045         d.addCallback(_check)
1046         return d
1047
1048     def test_unlink_with_nonexistent_alias(self):
1049         # 'tahoe unlink' should behave sensibly when invoked with an explicit
1050         # alias that doesn't exist.
1051         self.basedir = "cli/Unlink/%s_with_nonexistent_alias" % (self.command,)
1052         self.set_up_grid()
1053         d = self.do_cli(self.command, "nonexistent:afile")
1054         def _check((rc, out, err)):
1055             self.failUnlessReallyEqual(rc, 1)
1056             self.failUnlessIn("error:", err)
1057             self.failUnlessIn("nonexistent", err)
1058             self.failUnlessReallyEqual(out, "")
1059         d.addCallback(_check)
1060
1061         d.addCallback(lambda ign: self.do_cli(self.command, "nonexistent:afile"))
1062         d.addCallback(_check)
1063         return d
1064
1065     def test_unlink_without_path(self):
1066         # 'tahoe unlink' should give a sensible error message when invoked without a path.
1067         self.basedir = "cli/Unlink/%s_without_path" % (self.command,)
1068         self.set_up_grid()
1069         self._create_test_file()
1070         d = self.do_cli("create-alias", "tahoe")
1071         d.addCallback(lambda ign: self.do_cli("put", self.datafile, "tahoe:test"))
1072         def _do_unlink((rc, out, err)):
1073             self.failUnlessReallyEqual(rc, 0)
1074             self.failUnless(out.startswith("URI:"), out)
1075             return self.do_cli(self.command, out.strip('\n'))
1076         d.addCallback(_do_unlink)
1077
1078         def _check((rc, out, err)):
1079             self.failUnlessReallyEqual(rc, 1)
1080             self.failUnlessIn("'tahoe %s'" % (self.command,), err)
1081             self.failUnlessIn("path must be given", err)
1082             self.failUnlessReallyEqual(out, "")
1083         d.addCallback(_check)
1084         return d
1085
1086
1087 class Rm(Unlink):
1088     """Test that 'tahoe rm' behaves in the same way as 'tahoe unlink'."""
1089     command = "rm"
1090
1091
1092 class Stats(GridTestMixin, CLITestMixin, unittest.TestCase):
1093     def test_empty_directory(self):
1094         self.basedir = "cli/Stats/empty_directory"
1095         self.set_up_grid()
1096         c0 = self.g.clients[0]
1097         self.fileurls = {}
1098         d = c0.create_dirnode()
1099         def _stash_root(n):
1100             self.rootnode = n
1101             self.rooturi = n.get_uri()
1102         d.addCallback(_stash_root)
1103
1104         # make sure we can get stats on an empty directory too
1105         d.addCallback(lambda ign: self.do_cli("stats", self.rooturi))
1106         def _check_stats((rc, out, err)):
1107             self.failUnlessReallyEqual(err, "")
1108             self.failUnlessReallyEqual(rc, 0)
1109             lines = out.splitlines()
1110             self.failUnlessIn(" count-immutable-files: 0", lines)
1111             self.failUnlessIn("   count-mutable-files: 0", lines)
1112             self.failUnlessIn("   count-literal-files: 0", lines)
1113             self.failUnlessIn("     count-directories: 1", lines)
1114             self.failUnlessIn("  size-immutable-files: 0", lines)
1115             self.failIfIn("Size Histogram:", lines)
1116         d.addCallback(_check_stats)
1117
1118         return d
1119
1120     def test_stats_without_alias(self):
1121         # when invoked with no explicit alias and before the default 'tahoe'
1122         # alias is created, 'tahoe stats' should output an informative error
1123         # message, not a stack trace.
1124         self.basedir = "cli/Stats/stats_without_alias"
1125         self.set_up_grid()
1126         d = self.do_cli("stats")
1127         def _check((rc, out, err)):
1128             self.failUnlessReallyEqual(rc, 1)
1129             self.failUnlessIn("error:", err)
1130             self.failUnlessReallyEqual(out, "")
1131         d.addCallback(_check)
1132         return d
1133
1134     def test_stats_with_nonexistent_alias(self):
1135         # when invoked with an explicit alias that doesn't exist,
1136         # 'tahoe stats' should output a useful error message.
1137         self.basedir = "cli/Stats/stats_with_nonexistent_alias"
1138         self.set_up_grid()
1139         d = self.do_cli("stats", "havasu:")
1140         def _check((rc, out, err)):
1141             self.failUnlessReallyEqual(rc, 1)
1142             self.failUnlessIn("error:", err)
1143             self.failUnlessReallyEqual(out, "")
1144         d.addCallback(_check)
1145         return d
1146
1147
1148 class Webopen(GridTestMixin, CLITestMixin, unittest.TestCase):
1149     def test_webopen_with_nonexistent_alias(self):
1150         # when invoked with an alias that doesn't exist, 'tahoe webopen'
1151         # should output an informative error message instead of a stack
1152         # trace.
1153         self.basedir = "cli/Webopen/webopen_with_nonexistent_alias"
1154         self.set_up_grid()
1155         d = self.do_cli("webopen", "fake:")
1156         def _check((rc, out, err)):
1157             self.failUnlessReallyEqual(rc, 1)
1158             self.failUnlessIn("error:", err)
1159             self.failUnlessReallyEqual(out, "")
1160         d.addCallback(_check)
1161         return d
1162
1163     def test_webopen(self):
1164         # TODO: replace with @patch that supports Deferreds.
1165         import webbrowser
1166         def call_webbrowser_open(url):
1167             self.failUnlessIn(self.alias_uri.replace(':', '%3A'), url)
1168             self.webbrowser_open_called = True
1169         def _cleanup(res):
1170             webbrowser.open = self.old_webbrowser_open
1171             return res
1172
1173         self.old_webbrowser_open = webbrowser.open
1174         try:
1175             webbrowser.open = call_webbrowser_open
1176
1177             self.basedir = "cli/Webopen/webopen"
1178             self.set_up_grid()
1179             d = self.do_cli("create-alias", "alias:")
1180             def _check_alias((rc, out, err)):
1181                 self.failUnlessReallyEqual(rc, 0, repr((rc, out, err)))
1182                 self.failUnlessIn("Alias 'alias' created", out)
1183                 self.failUnlessReallyEqual(err, "")
1184                 self.alias_uri = get_aliases(self.get_clientdir())["alias"]
1185             d.addCallback(_check_alias)
1186             d.addCallback(lambda res: self.do_cli("webopen", "alias:"))
1187             def _check_webopen((rc, out, err)):
1188                 self.failUnlessReallyEqual(rc, 0, repr((rc, out, err)))
1189                 self.failUnlessReallyEqual(out, "")
1190                 self.failUnlessReallyEqual(err, "")
1191                 self.failUnless(self.webbrowser_open_called)
1192             d.addCallback(_check_webopen)
1193             d.addBoth(_cleanup)
1194         except:
1195             _cleanup(None)
1196             raise
1197         return d
1198
1199 class Options(ReallyEqualMixin, unittest.TestCase):
1200     # this test case only looks at argument-processing and simple stuff.
1201
1202     def parse(self, args, stdout=None):
1203         o = runner.Options()
1204         if stdout is not None:
1205             o.stdout = stdout
1206         o.parseOptions(args)
1207         while hasattr(o, "subOptions"):
1208             o = o.subOptions
1209         return o
1210
1211     def test_list(self):
1212         fileutil.rm_dir("cli/test_options")
1213         fileutil.make_dirs("cli/test_options")
1214         fileutil.make_dirs("cli/test_options/private")
1215         fileutil.write("cli/test_options/node.url", "http://localhost:8080/\n")
1216         filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
1217                                                fingerprint="\x00"*32)
1218         private_uri = uri.DirectoryURI(filenode_uri).to_string()
1219         fileutil.write("cli/test_options/private/root_dir.cap", private_uri + "\n")
1220         def parse2(args): return parse_options("cli/test_options", "ls", args)
1221         o = parse2([])
1222         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1223         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
1224         self.failUnlessEqual(o.where, u"")
1225
1226         o = parse2(["--node-url", "http://example.org:8111/"])
1227         self.failUnlessEqual(o['node-url'], "http://example.org:8111/")
1228         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
1229         self.failUnlessEqual(o.where, u"")
1230
1231         o = parse2(["--dir-cap", "root"])
1232         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1233         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], "root")
1234         self.failUnlessEqual(o.where, u"")
1235
1236         other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
1237                                                      fingerprint="\x11"*32)
1238         other_uri = uri.DirectoryURI(other_filenode_uri).to_string()
1239         o = parse2(["--dir-cap", other_uri])
1240         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1241         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
1242         self.failUnlessEqual(o.where, u"")
1243
1244         o = parse2(["--dir-cap", other_uri, "subdir"])
1245         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
1246         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
1247         self.failUnlessEqual(o.where, u"subdir")
1248
1249         self.failUnlessRaises(usage.UsageError, parse2,
1250                               ["--node-url", "NOT-A-URL"])
1251
1252         o = parse2(["--node-url", "http://localhost:8080"])
1253         self.failUnlessEqual(o["node-url"], "http://localhost:8080/")
1254
1255         o = parse2(["--node-url", "https://localhost/"])
1256         self.failUnlessEqual(o["node-url"], "https://localhost/")
1257
1258     def test_version(self):
1259         # "tahoe --version" dumps text to stdout and exits
1260         stdout = StringIO()
1261         self.failUnlessRaises(SystemExit, self.parse, ["--version"], stdout)
1262         self.failUnlessIn(allmydata.__appname__ + ":", stdout.getvalue())
1263         # but "tahoe SUBCOMMAND --version" should be rejected
1264         self.failUnlessRaises(usage.UsageError, self.parse,
1265                               ["start", "--version"])
1266         self.failUnlessRaises(usage.UsageError, self.parse,
1267                               ["start", "--version-and-path"])
1268
1269     def test_quiet(self):
1270         # accepted as an overall option, but not on subcommands
1271         o = self.parse(["--quiet", "start"])
1272         self.failUnless(o.parent["quiet"])
1273         self.failUnlessRaises(usage.UsageError, self.parse,
1274                               ["start", "--quiet"])
1275
1276     def test_basedir(self):
1277         # accept a --node-directory option before the verb, or a --basedir
1278         # option after, or a basedir argument after, but none in the wrong
1279         # place, and not more than one of the three.
1280         o = self.parse(["start"])
1281         self.failUnlessReallyEqual(o["basedir"], os.path.join(fileutil.abspath_expanduser_unicode(u"~"),
1282                                                               u".tahoe"))
1283         o = self.parse(["start", "here"])
1284         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"here"))
1285         o = self.parse(["start", "--basedir", "there"])
1286         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"there"))
1287         o = self.parse(["--node-directory", "there", "start"])
1288         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"there"))
1289
1290         o = self.parse(["start", "here", "--nodaemon"])
1291         self.failUnlessReallyEqual(o["basedir"], fileutil.abspath_expanduser_unicode(u"here"))
1292
1293         self.failUnlessRaises(usage.UsageError, self.parse,
1294                               ["--basedir", "there", "start"])
1295         self.failUnlessRaises(usage.UsageError, self.parse,
1296                               ["start", "--node-directory", "there"])
1297
1298         self.failUnlessRaises(usage.UsageError, self.parse,
1299                               ["--node-directory=there",
1300                                "start", "--basedir=here"])
1301         self.failUnlessRaises(usage.UsageError, self.parse,
1302                               ["start", "--basedir=here", "anywhere"])
1303         self.failUnlessRaises(usage.UsageError, self.parse,
1304                               ["--node-directory=there",
1305                                "start", "anywhere"])
1306         self.failUnlessRaises(usage.UsageError, self.parse,
1307                               ["--node-directory=there",
1308                                "start", "--basedir=here", "anywhere"])
1309
1310         self.failUnlessRaises(usage.UsageError, self.parse,
1311                               ["--node-directory=there", "start", "--nodaemon"])
1312         self.failUnlessRaises(usage.UsageError, self.parse,
1313                               ["start", "--basedir=here", "--nodaemon"])