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