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