]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_cli.py
test_cli.py: make 'tahoe mkdir' tests slightly less dumb (check for 'URI:' in the...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_cli.py
1
2 import os.path
3 from twisted.trial import unittest
4 from cStringIO import StringIO
5 import urllib, re
6 import simplejson
7
8 from allmydata.util import fileutil, hashutil, base32
9 from allmydata import uri
10 from allmydata.immutable import upload
11 from allmydata.dirnode import normalize
12
13 # Test that the scripts can be imported -- although the actual tests of their
14 # functionality are done by invoking them in a subprocess.
15 from allmydata.scripts import create_node, debug, keygen, startstop_node, \
16     tahoe_add_alias, tahoe_backup, tahoe_check, tahoe_cp, tahoe_get, tahoe_ls, \
17     tahoe_manifest, tahoe_mkdir, tahoe_mv, tahoe_put, tahoe_rm, tahoe_webopen
18 _hush_pyflakes = [create_node, debug, keygen, startstop_node,
19     tahoe_add_alias, tahoe_backup, tahoe_check, tahoe_cp, tahoe_get, tahoe_ls,
20     tahoe_manifest, tahoe_mkdir, tahoe_mv, tahoe_put, tahoe_rm, tahoe_webopen]
21
22 from allmydata.scripts import common
23 from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases, get_alias, \
24      DefaultAliasMarker
25
26 from allmydata.scripts import cli, debug, runner, backupdb
27 from allmydata.test.common_util import StallMixin, ReallyEqualMixin
28 from allmydata.test.no_network import GridTestMixin
29 from twisted.internet import threads # CLI tests use deferToThread
30 from twisted.python import usage
31
32 from allmydata.util.assertutil import precondition
33 from allmydata.util.encodingutil import listdir_unicode, unicode_platform, \
34     quote_output, get_output_encoding, get_argv_encoding, get_filesystem_encoding, \
35     unicode_to_output, unicode_to_argv, to_str
36 from allmydata.util.fileutil import abspath_expanduser_unicode
37
38 timeout = 480 # deep_check takes 360s on Zandr's linksys box, others take > 240s
39
40
41 class CLITestMixin(ReallyEqualMixin):
42     def do_cli(self, verb, *args, **kwargs):
43         nodeargs = [
44             "--node-directory", self.get_clientdir(),
45             ]
46         argv = [verb] + nodeargs + list(args)
47         stdin = kwargs.get("stdin", "")
48         stdout, stderr = StringIO(), StringIO()
49         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
50                                   stdin=StringIO(stdin),
51                                   stdout=stdout, stderr=stderr)
52         def _done(rc):
53             return rc, stdout.getvalue(), stderr.getvalue()
54         d.addCallback(_done)
55         return d
56
57     def skip_if_cannot_represent_filename(self, u):
58         precondition(isinstance(u, unicode))
59
60         enc = get_filesystem_encoding()
61         if not unicode_platform():
62             try:
63                 u.encode(enc)
64             except UnicodeEncodeError:
65                 raise unittest.SkipTest("A non-ASCII filename could not be encoded on this platform.")
66
67
68 class CLI(CLITestMixin, unittest.TestCase):
69     # this test case only looks at argument-processing and simple stuff.
70     def test_options(self):
71         fileutil.rm_dir("cli/test_options")
72         fileutil.make_dirs("cli/test_options")
73         fileutil.make_dirs("cli/test_options/private")
74         fileutil.write("cli/test_options/node.url", "http://localhost:8080/\n")
75         filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
76                                                fingerprint="\x00"*32)
77         private_uri = uri.DirectoryURI(filenode_uri).to_string()
78         fileutil.write("cli/test_options/private/root_dir.cap", private_uri + "\n")
79         o = cli.ListOptions()
80         o.parseOptions(["--node-directory", "cli/test_options"])
81         self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
82         self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], private_uri)
83         self.failUnlessReallyEqual(o.where, u"")
84
85         o = cli.ListOptions()
86         o.parseOptions(["--node-directory", "cli/test_options",
87                         "--node-url", "http://example.org:8111/"])
88         self.failUnlessReallyEqual(o['node-url'], "http://example.org:8111/")
89         self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], private_uri)
90         self.failUnlessReallyEqual(o.where, u"")
91
92         o = cli.ListOptions()
93         o.parseOptions(["--node-directory", "cli/test_options",
94                         "--dir-cap", "root"])
95         self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
96         self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], "root")
97         self.failUnlessReallyEqual(o.where, u"")
98
99         o = cli.ListOptions()
100         other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
101                                                      fingerprint="\x11"*32)
102         other_uri = uri.DirectoryURI(other_filenode_uri).to_string()
103         o.parseOptions(["--node-directory", "cli/test_options",
104                         "--dir-cap", other_uri])
105         self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
106         self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], other_uri)
107         self.failUnlessReallyEqual(o.where, u"")
108
109         o = cli.ListOptions()
110         o.parseOptions(["--node-directory", "cli/test_options",
111                         "--dir-cap", other_uri, "subdir"])
112         self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
113         self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], other_uri)
114         self.failUnlessReallyEqual(o.where, u"subdir")
115
116         o = cli.ListOptions()
117         self.failUnlessRaises(usage.UsageError,
118                               o.parseOptions,
119                               ["--node-directory", "cli/test_options",
120                                "--node-url", "NOT-A-URL"])
121
122         o = cli.ListOptions()
123         o.parseOptions(["--node-directory", "cli/test_options",
124                         "--node-url", "http://localhost:8080"])
125         self.failUnlessReallyEqual(o["node-url"], "http://localhost:8080/")
126
127         o = cli.ListOptions()
128         o.parseOptions(["--node-directory", "cli/test_options",
129                         "--node-url", "https://localhost/"])
130         self.failUnlessReallyEqual(o["node-url"], "https://localhost/")
131
132     def _dump_cap(self, *args):
133         config = debug.DumpCapOptions()
134         config.stdout,config.stderr = StringIO(), StringIO()
135         config.parseOptions(args)
136         debug.dump_cap(config)
137         self.failIf(config.stderr.getvalue())
138         output = config.stdout.getvalue()
139         return output
140
141     def test_dump_cap_chk(self):
142         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
143         uri_extension_hash = hashutil.uri_extension_hash("stuff")
144         needed_shares = 25
145         total_shares = 100
146         size = 1234
147         u = uri.CHKFileURI(key=key,
148                            uri_extension_hash=uri_extension_hash,
149                            needed_shares=needed_shares,
150                            total_shares=total_shares,
151                            size=size)
152         output = self._dump_cap(u.to_string())
153         self.failUnless("CHK File:" in output, output)
154         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
155         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
156         self.failUnless("size: 1234" in output, output)
157         self.failUnless("k/N: 25/100" in output, output)
158         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
159
160         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
161                                 u.to_string())
162         self.failUnless("client renewal secret: znxmki5zdibb5qlt46xbdvk2t55j7hibejq3i5ijyurkr6m6jkhq" in output, output)
163
164         output = self._dump_cap(u.get_verify_cap().to_string())
165         self.failIf("key: " in output, output)
166         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
167         self.failUnless("size: 1234" in output, output)
168         self.failUnless("k/N: 25/100" in output, output)
169         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
170
171         prefixed_u = "http://127.0.0.1/uri/%s" % urllib.quote(u.to_string())
172         output = self._dump_cap(prefixed_u)
173         self.failUnless("CHK File:" in output, output)
174         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
175         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
176         self.failUnless("size: 1234" in output, output)
177         self.failUnless("k/N: 25/100" in output, output)
178         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
179
180     def test_dump_cap_lit(self):
181         u = uri.LiteralFileURI("this is some data")
182         output = self._dump_cap(u.to_string())
183         self.failUnless("Literal File URI:" in output, output)
184         self.failUnless("data: 'this is some data'" in output, output)
185
186     def test_dump_cap_ssk(self):
187         writekey = "\x01" * 16
188         fingerprint = "\xfe" * 32
189         u = uri.WriteableSSKFileURI(writekey, fingerprint)
190
191         output = self._dump_cap(u.to_string())
192         self.failUnless("SSK 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("SSK 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("SSK Verifier URI:" in output, output)
234         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
235         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
236
237     def test_dump_cap_directory(self):
238         writekey = "\x01" * 16
239         fingerprint = "\xfe" * 32
240         u1 = uri.WriteableSSKFileURI(writekey, fingerprint)
241         u = uri.DirectoryURI(u1)
242
243         output = self._dump_cap(u.to_string())
244         self.failUnless("Directory Writeable URI:" in output, output)
245         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output,
246                         output)
247         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
248         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output,
249                         output)
250         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
251
252         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
253                                 u.to_string())
254         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
255
256         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
257                                 u.to_string())
258         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
259         self.failIf("file renewal secret:" in output, output)
260
261         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
262                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
263                                 u.to_string())
264         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
265         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
266         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
267
268         u = u.get_readonly()
269         output = self._dump_cap(u.to_string())
270         self.failUnless("Directory Read-only URI:" in output, output)
271         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
272         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
273         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
274
275         u = u.get_verify_cap()
276         output = self._dump_cap(u.to_string())
277         self.failUnless("Directory Verifier URI:" in output, output)
278         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
279         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
280
281     def _catalog_shares(self, *basedirs):
282         o = debug.CatalogSharesOptions()
283         o.stdout,o.stderr = StringIO(), StringIO()
284         args = list(basedirs)
285         o.parseOptions(args)
286         debug.catalog_shares(o)
287         out = o.stdout.getvalue()
288         err = o.stderr.getvalue()
289         return out, err
290
291     def test_catalog_shares_error(self):
292         nodedir1 = "cli/test_catalog_shares/node1"
293         sharedir = os.path.join(nodedir1, "storage", "shares", "mq", "mqfblse6m5a6dh45isu2cg7oji")
294         fileutil.make_dirs(sharedir)
295         fileutil.write("cli/test_catalog_shares/node1/storage/shares/mq/not-a-dir", "")
296         # write a bogus share that looks a little bit like CHK
297         fileutil.write(os.path.join(sharedir, "8"),
298                        "\x00\x00\x00\x01" + "\xff" * 200) # this triggers an assert
299
300         nodedir2 = "cli/test_catalog_shares/node2"
301         fileutil.make_dirs(nodedir2)
302         fileutil.write("cli/test_catalog_shares/node1/storage/shares/not-a-dir", "")
303
304         # now make sure that the 'catalog-shares' commands survives the error
305         out, err = self._catalog_shares(nodedir1, nodedir2)
306         self.failUnlessReallyEqual(out, "", out)
307         self.failUnless("Error processing " in err,
308                         "didn't see 'error processing' in '%s'" % err)
309         #self.failUnless(nodedir1 in err,
310         #                "didn't see '%s' in '%s'" % (nodedir1, err))
311         # windows mangles the path, and os.path.join isn't enough to make
312         # up for it, so just look for individual strings
313         self.failUnless("node1" in err,
314                         "didn't see 'node1' in '%s'" % err)
315         self.failUnless("mqfblse6m5a6dh45isu2cg7oji" in err,
316                         "didn't see 'mqfblse6m5a6dh45isu2cg7oji' in '%s'" % err)
317
318     def test_alias(self):
319         aliases = {"tahoe": "TA",
320                    "work": "WA",
321                    "c": "CA"}
322         def ga1(path):
323             return get_alias(aliases, path, u"tahoe")
324         uses_lettercolon = common.platform_uses_lettercolon_drivename()
325         self.failUnlessReallyEqual(ga1(u"bare"), ("TA", "bare"))
326         self.failUnlessReallyEqual(ga1(u"baredir/file"), ("TA", "baredir/file"))
327         self.failUnlessReallyEqual(ga1(u"baredir/file:7"), ("TA", "baredir/file:7"))
328         self.failUnlessReallyEqual(ga1(u"tahoe:"), ("TA", ""))
329         self.failUnlessReallyEqual(ga1(u"tahoe:file"), ("TA", "file"))
330         self.failUnlessReallyEqual(ga1(u"tahoe:dir/file"), ("TA", "dir/file"))
331         self.failUnlessReallyEqual(ga1(u"work:"), ("WA", ""))
332         self.failUnlessReallyEqual(ga1(u"work:file"), ("WA", "file"))
333         self.failUnlessReallyEqual(ga1(u"work:dir/file"), ("WA", "dir/file"))
334         # default != None means we really expect a tahoe path, regardless of
335         # whether we're on windows or not. This is what 'tahoe get' uses.
336         self.failUnlessReallyEqual(ga1(u"c:"), ("CA", ""))
337         self.failUnlessReallyEqual(ga1(u"c:file"), ("CA", "file"))
338         self.failUnlessReallyEqual(ga1(u"c:dir/file"), ("CA", "dir/file"))
339         self.failUnlessReallyEqual(ga1(u"URI:stuff"), ("URI:stuff", ""))
340         self.failUnlessReallyEqual(ga1(u"URI:stuff/file"), ("URI:stuff", "file"))
341         self.failUnlessReallyEqual(ga1(u"URI:stuff:./file"), ("URI:stuff", "file"))
342         self.failUnlessReallyEqual(ga1(u"URI:stuff/dir/file"), ("URI:stuff", "dir/file"))
343         self.failUnlessReallyEqual(ga1(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
344         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:")
345         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:dir")
346         self.failUnlessRaises(common.UnknownAliasError, ga1, u"missing:dir/file")
347
348         def ga2(path):
349             return get_alias(aliases, path, None)
350         self.failUnlessReallyEqual(ga2(u"bare"), (DefaultAliasMarker, "bare"))
351         self.failUnlessReallyEqual(ga2(u"baredir/file"),
352                              (DefaultAliasMarker, "baredir/file"))
353         self.failUnlessReallyEqual(ga2(u"baredir/file:7"),
354                              (DefaultAliasMarker, "baredir/file:7"))
355         self.failUnlessReallyEqual(ga2(u"baredir/sub:1/file:7"),
356                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
357         self.failUnlessReallyEqual(ga2(u"tahoe:"), ("TA", ""))
358         self.failUnlessReallyEqual(ga2(u"tahoe:file"), ("TA", "file"))
359         self.failUnlessReallyEqual(ga2(u"tahoe:dir/file"), ("TA", "dir/file"))
360         # on windows, we really want c:foo to indicate a local file.
361         # default==None is what 'tahoe cp' uses.
362         if uses_lettercolon:
363             self.failUnlessReallyEqual(ga2(u"c:"), (DefaultAliasMarker, "c:"))
364             self.failUnlessReallyEqual(ga2(u"c:file"), (DefaultAliasMarker, "c:file"))
365             self.failUnlessReallyEqual(ga2(u"c:dir/file"),
366                                  (DefaultAliasMarker, "c:dir/file"))
367         else:
368             self.failUnlessReallyEqual(ga2(u"c:"), ("CA", ""))
369             self.failUnlessReallyEqual(ga2(u"c:file"), ("CA", "file"))
370             self.failUnlessReallyEqual(ga2(u"c:dir/file"), ("CA", "dir/file"))
371         self.failUnlessReallyEqual(ga2(u"work:"), ("WA", ""))
372         self.failUnlessReallyEqual(ga2(u"work:file"), ("WA", "file"))
373         self.failUnlessReallyEqual(ga2(u"work:dir/file"), ("WA", "dir/file"))
374         self.failUnlessReallyEqual(ga2(u"URI:stuff"), ("URI:stuff", ""))
375         self.failUnlessReallyEqual(ga2(u"URI:stuff/file"), ("URI:stuff", "file"))
376         self.failUnlessReallyEqual(ga2(u"URI:stuff:./file"), ("URI:stuff", "file"))
377         self.failUnlessReallyEqual(ga2(u"URI:stuff/dir/file"), ("URI:stuff", "dir/file"))
378         self.failUnlessReallyEqual(ga2(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
379         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:")
380         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:dir")
381         self.failUnlessRaises(common.UnknownAliasError, ga2, u"missing:dir/file")
382
383         def ga3(path):
384             old = common.pretend_platform_uses_lettercolon
385             try:
386                 common.pretend_platform_uses_lettercolon = True
387                 retval = get_alias(aliases, path, None)
388             finally:
389                 common.pretend_platform_uses_lettercolon = old
390             return retval
391         self.failUnlessReallyEqual(ga3(u"bare"), (DefaultAliasMarker, "bare"))
392         self.failUnlessReallyEqual(ga3(u"baredir/file"),
393                              (DefaultAliasMarker, "baredir/file"))
394         self.failUnlessReallyEqual(ga3(u"baredir/file:7"),
395                              (DefaultAliasMarker, "baredir/file:7"))
396         self.failUnlessReallyEqual(ga3(u"baredir/sub:1/file:7"),
397                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
398         self.failUnlessReallyEqual(ga3(u"tahoe:"), ("TA", ""))
399         self.failUnlessReallyEqual(ga3(u"tahoe:file"), ("TA", "file"))
400         self.failUnlessReallyEqual(ga3(u"tahoe:dir/file"), ("TA", "dir/file"))
401         self.failUnlessReallyEqual(ga3(u"c:"), (DefaultAliasMarker, "c:"))
402         self.failUnlessReallyEqual(ga3(u"c:file"), (DefaultAliasMarker, "c:file"))
403         self.failUnlessReallyEqual(ga3(u"c:dir/file"),
404                              (DefaultAliasMarker, "c:dir/file"))
405         self.failUnlessReallyEqual(ga3(u"work:"), ("WA", ""))
406         self.failUnlessReallyEqual(ga3(u"work:file"), ("WA", "file"))
407         self.failUnlessReallyEqual(ga3(u"work:dir/file"), ("WA", "dir/file"))
408         self.failUnlessReallyEqual(ga3(u"URI:stuff"), ("URI:stuff", ""))
409         self.failUnlessReallyEqual(ga3(u"URI:stuff:./file"), ("URI:stuff", "file"))
410         self.failUnlessReallyEqual(ga3(u"URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
411         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:")
412         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:dir")
413         self.failUnlessRaises(common.UnknownAliasError, ga3, u"missing:dir/file")
414         # calling get_alias with a path that doesn't include an alias and
415         # default set to something that isn't in the aliases argument should
416         # raise an UnknownAliasError.
417         def ga4(path):
418             return get_alias(aliases, path, u"badddefault:")
419         self.failUnlessRaises(common.UnknownAliasError, ga4, u"afile")
420         self.failUnlessRaises(common.UnknownAliasError, ga4, u"a/dir/path/")
421
422         def ga5(path):
423             old = common.pretend_platform_uses_lettercolon
424             try:
425                 common.pretend_platform_uses_lettercolon = True
426                 retval = get_alias(aliases, path, u"baddefault:")
427             finally:
428                 common.pretend_platform_uses_lettercolon = old
429             return retval
430         self.failUnlessRaises(common.UnknownAliasError, ga5, u"C:\\Windows")
431
432     def test_listdir_unicode_good(self):
433         filenames = [u'L\u00F4zane', u'Bern', u'Gen\u00E8ve']  # must be NFC
434
435         for name in filenames:
436             self.skip_if_cannot_represent_filename(name)
437
438         basedir = "cli/common/listdir_unicode_good"
439         fileutil.make_dirs(basedir)
440
441         for name in filenames:
442             open(os.path.join(unicode(basedir), name), "wb").close()
443
444         for file in listdir_unicode(unicode(basedir)):
445             self.failUnlessIn(normalize(file), filenames)
446
447
448 class Help(unittest.TestCase):
449
450     def test_get(self):
451         help = str(cli.GetOptions())
452         self.failUnless("get REMOTE_FILE LOCAL_FILE" in help, help)
453         self.failUnless("% tahoe get FOO |less" in help, help)
454
455     def test_put(self):
456         help = str(cli.PutOptions())
457         self.failUnless("put LOCAL_FILE REMOTE_FILE" in help, help)
458         self.failUnless("% cat FILE | tahoe put" in help, help)
459
460     def test_rm(self):
461         help = str(cli.RmOptions())
462         self.failUnless("rm REMOTE_FILE" in help, help)
463
464     def test_mv(self):
465         help = str(cli.MvOptions())
466         self.failUnless("mv FROM TO" in help, help)
467         self.failUnless("Use 'tahoe mv' to move files" in help)
468
469     def test_ln(self):
470         help = str(cli.LnOptions())
471         self.failUnless("ln FROM TO" in help, help)
472
473     def test_backup(self):
474         help = str(cli.BackupOptions())
475         self.failUnless("backup FROM ALIAS:TO" in help, help)
476
477     def test_webopen(self):
478         help = str(cli.WebopenOptions())
479         self.failUnless("webopen [ALIAS:PATH]" in help, help)
480
481     def test_manifest(self):
482         help = str(cli.ManifestOptions())
483         self.failUnless("manifest [ALIAS:PATH]" in help, help)
484
485     def test_stats(self):
486         help = str(cli.StatsOptions())
487         self.failUnless("stats [ALIAS:PATH]" in help, help)
488
489     def test_check(self):
490         help = str(cli.CheckOptions())
491         self.failUnless("check [ALIAS:PATH]" in help, help)
492
493     def test_deep_check(self):
494         help = str(cli.DeepCheckOptions())
495         self.failUnless("deep-check [ALIAS:PATH]" in help, help)
496
497     def test_create_alias(self):
498         help = str(cli.CreateAliasOptions())
499         self.failUnless("create-alias ALIAS" in help, help)
500
501     def test_add_aliases(self):
502         help = str(cli.AddAliasOptions())
503         self.failUnless("add-alias ALIAS DIRCAP" in help, help)
504
505
506 class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase):
507
508     def _test_webopen(self, args, expected_url):
509         woo = cli.WebopenOptions()
510         all_args = ["--node-directory", self.get_clientdir()] + list(args)
511         woo.parseOptions(all_args)
512         urls = []
513         rc = cli.webopen(woo, urls.append)
514         self.failUnlessReallyEqual(rc, 0)
515         self.failUnlessReallyEqual(len(urls), 1)
516         self.failUnlessReallyEqual(urls[0], expected_url)
517
518     def test_create(self):
519         self.basedir = "cli/CreateAlias/create"
520         self.set_up_grid()
521         aliasfile = os.path.join(self.get_clientdir(), "private", "aliases")
522
523         d = self.do_cli("create-alias", "tahoe")
524         def _done((rc,stdout,stderr)):
525             self.failUnless("Alias 'tahoe' created" in stdout)
526             self.failIf(stderr)
527             aliases = get_aliases(self.get_clientdir())
528             self.failUnless("tahoe" in aliases)
529             self.failUnless(aliases["tahoe"].startswith("URI:DIR2:"))
530         d.addCallback(_done)
531         d.addCallback(lambda res: self.do_cli("create-alias", "two"))
532
533         def _stash_urls(res):
534             aliases = get_aliases(self.get_clientdir())
535             node_url_file = os.path.join(self.get_clientdir(), "node.url")
536             nodeurl = fileutil.read(node_url_file).strip()
537             self.welcome_url = nodeurl
538             uribase = nodeurl + "uri/"
539             self.tahoe_url = uribase + urllib.quote(aliases["tahoe"])
540             self.tahoe_subdir_url = self.tahoe_url + "/subdir"
541             self.two_url = uribase + urllib.quote(aliases["two"])
542             self.two_uri = aliases["two"]
543         d.addCallback(_stash_urls)
544
545         d.addCallback(lambda res: self.do_cli("create-alias", "two")) # dup
546         def _check_create_duplicate((rc,stdout,stderr)):
547             self.failIfEqual(rc, 0)
548             self.failUnless("Alias 'two' already exists!" in stderr)
549             aliases = get_aliases(self.get_clientdir())
550             self.failUnlessReallyEqual(aliases["two"], self.two_uri)
551         d.addCallback(_check_create_duplicate)
552
553         d.addCallback(lambda res: self.do_cli("add-alias", "added", self.two_uri))
554         def _check_add((rc,stdout,stderr)):
555             self.failUnlessReallyEqual(rc, 0)
556             self.failUnless("Alias 'added' added" in stdout)
557         d.addCallback(_check_add)
558
559         # check add-alias with a duplicate
560         d.addCallback(lambda res: self.do_cli("add-alias", "two", self.two_uri))
561         def _check_add_duplicate((rc,stdout,stderr)):
562             self.failIfEqual(rc, 0)
563             self.failUnless("Alias 'two' already exists!" in stderr)
564             aliases = get_aliases(self.get_clientdir())
565             self.failUnlessReallyEqual(aliases["two"], self.two_uri)
566         d.addCallback(_check_add_duplicate)
567
568         def _test_urls(junk):
569             self._test_webopen([], self.welcome_url)
570             self._test_webopen(["/"], self.tahoe_url)
571             self._test_webopen(["tahoe:"], self.tahoe_url)
572             self._test_webopen(["tahoe:/"], self.tahoe_url)
573             self._test_webopen(["tahoe:subdir"], self.tahoe_subdir_url)
574             self._test_webopen(["-i", "tahoe:subdir"],
575                                self.tahoe_subdir_url+"?t=info")
576             self._test_webopen(["tahoe:subdir/"], self.tahoe_subdir_url + '/')
577             self._test_webopen(["tahoe:subdir/file"],
578                                self.tahoe_subdir_url + '/file')
579             self._test_webopen(["--info", "tahoe:subdir/file"],
580                                self.tahoe_subdir_url + '/file?t=info')
581             # if "file" is indeed a file, then the url produced by webopen in
582             # this case is disallowed by the webui. but by design, webopen
583             # passes through the mistake from the user to the resultant
584             # webopened url
585             self._test_webopen(["tahoe:subdir/file/"], self.tahoe_subdir_url + '/file/')
586             self._test_webopen(["two:"], self.two_url)
587         d.addCallback(_test_urls)
588
589         def _remove_trailing_newline_and_create_alias(ign):
590             # ticket #741 is about a manually-edited alias file (which
591             # doesn't end in a newline) being corrupted by a subsequent
592             # "tahoe create-alias"
593             old = fileutil.read(aliasfile)
594             fileutil.write(aliasfile, old.rstrip())
595             return self.do_cli("create-alias", "un-corrupted1")
596         d.addCallback(_remove_trailing_newline_and_create_alias)
597         def _check_not_corrupted1((rc,stdout,stderr)):
598             self.failUnless("Alias 'un-corrupted1' created" in stdout, stdout)
599             self.failIf(stderr)
600             # the old behavior was to simply append the new record, causing a
601             # line that looked like "NAME1: CAP1NAME2: CAP2". This won't look
602             # like a valid dircap, so get_aliases() will raise an exception.
603             aliases = get_aliases(self.get_clientdir())
604             self.failUnless("added" in aliases)
605             self.failUnless(aliases["added"].startswith("URI:DIR2:"))
606             # to be safe, let's confirm that we don't see "NAME2:" in CAP1.
607             # No chance of a false-negative, because the hyphen in
608             # "un-corrupted1" is not a valid base32 character.
609             self.failIfIn("un-corrupted1:", aliases["added"])
610             self.failUnless("un-corrupted1" in aliases)
611             self.failUnless(aliases["un-corrupted1"].startswith("URI:DIR2:"))
612         d.addCallback(_check_not_corrupted1)
613
614         def _remove_trailing_newline_and_add_alias(ign):
615             # same thing, but for "tahoe add-alias"
616             old = fileutil.read(aliasfile)
617             fileutil.write(aliasfile, old.rstrip())
618             return self.do_cli("add-alias", "un-corrupted2", self.two_uri)
619         d.addCallback(_remove_trailing_newline_and_add_alias)
620         def _check_not_corrupted((rc,stdout,stderr)):
621             self.failUnless("Alias 'un-corrupted2' added" in stdout, stdout)
622             self.failIf(stderr)
623             aliases = get_aliases(self.get_clientdir())
624             self.failUnless("un-corrupted1" in aliases)
625             self.failUnless(aliases["un-corrupted1"].startswith("URI:DIR2:"))
626             self.failIfIn("un-corrupted2:", aliases["un-corrupted1"])
627             self.failUnless("un-corrupted2" in aliases)
628             self.failUnless(aliases["un-corrupted2"].startswith("URI:DIR2:"))
629         d.addCallback(_check_not_corrupted)
630
631     def test_create_unicode(self):
632         self.basedir = "cli/CreateAlias/create_unicode"
633         self.set_up_grid()
634
635         try:
636             etudes_arg = u"\u00E9tudes".encode(get_argv_encoding())
637             lumiere_arg = u"lumi\u00E8re.txt".encode(get_argv_encoding())
638         except UnicodeEncodeError:
639             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
640
641         d = self.do_cli("create-alias", etudes_arg)
642         def _check_create_unicode((rc, out, err)):
643             self.failUnlessReallyEqual(rc, 0)
644             self.failUnlessReallyEqual(err, "")
645             self.failUnlessIn("Alias %s created" % quote_output(u"\u00E9tudes"), out)
646
647             aliases = get_aliases(self.get_clientdir())
648             self.failUnless(aliases[u"\u00E9tudes"].startswith("URI:DIR2:"))
649         d.addCallback(_check_create_unicode)
650
651         d.addCallback(lambda res: self.do_cli("ls", etudes_arg + ":"))
652         def _check_ls1((rc, out, err)):
653             self.failUnlessReallyEqual(rc, 0)
654             self.failUnlessReallyEqual(err, "")
655             self.failUnlessReallyEqual(out, "")
656         d.addCallback(_check_ls1)
657
658         d.addCallback(lambda res: self.do_cli("put", "-", etudes_arg + ":uploaded.txt",
659                                               stdin="Blah blah blah"))
660
661         d.addCallback(lambda res: self.do_cli("ls", etudes_arg + ":"))
662         def _check_ls2((rc, out, err)):
663             self.failUnlessReallyEqual(rc, 0)
664             self.failUnlessReallyEqual(err, "")
665             self.failUnlessReallyEqual(out, "uploaded.txt\n")
666         d.addCallback(_check_ls2)
667
668         d.addCallback(lambda res: self.do_cli("get", etudes_arg + ":uploaded.txt"))
669         def _check_get((rc, out, err)):
670             self.failUnlessReallyEqual(rc, 0)
671             self.failUnlessReallyEqual(err, "")
672             self.failUnlessReallyEqual(out, "Blah blah blah")
673         d.addCallback(_check_get)
674
675         # Ensure that an Unicode filename in an Unicode alias works as expected
676         d.addCallback(lambda res: self.do_cli("put", "-", etudes_arg + ":" + lumiere_arg,
677                                               stdin="Let the sunshine In!"))
678
679         d.addCallback(lambda res: self.do_cli("get",
680                                               get_aliases(self.get_clientdir())[u"\u00E9tudes"] + "/" + lumiere_arg))
681         def _check_get2((rc, out, err)):
682             self.failUnlessReallyEqual(rc, 0)
683             self.failUnlessReallyEqual(err, "")
684             self.failUnlessReallyEqual(out, "Let the sunshine In!")
685         d.addCallback(_check_get2)
686
687         return d
688
689     # TODO: test list-aliases, including Unicode
690
691
692 class Ln(GridTestMixin, CLITestMixin, unittest.TestCase):
693     def _create_test_file(self):
694         data = "puppies" * 1000
695         path = os.path.join(self.basedir, "datafile")
696         fileutil.write(path, data)
697         self.datafile = path
698
699     def test_ln_without_alias(self):
700         # if invoked without an alias when the 'tahoe' alias doesn't
701         # exist, 'tahoe ln' should output a useful error message and not
702         # a stack trace
703         self.basedir = "cli/Ln/ln_without_alias"
704         self.set_up_grid()
705         d = self.do_cli("ln", "from", "to")
706         def _check((rc, out, err)):
707             self.failUnlessReallyEqual(rc, 1)
708             self.failUnlessIn("error:", err)
709             self.failUnlessReallyEqual(out, "")
710         d.addCallback(_check)
711         # Make sure that validation extends to the "to" parameter
712         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
713         d.addCallback(lambda ign: self._create_test_file())
714         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
715                                               "havasu:from"))
716         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "to"))
717         d.addCallback(_check)
718         return d
719
720     def test_ln_with_nonexistent_alias(self):
721         # If invoked with aliases that don't exist, 'tahoe ln' should
722         # output a useful error message and not a stack trace.
723         self.basedir = "cli/Ln/ln_with_nonexistent_alias"
724         self.set_up_grid()
725         d = self.do_cli("ln", "havasu:from", "havasu:to")
726         def _check((rc, out, err)):
727             self.failUnlessReallyEqual(rc, 1)
728             self.failUnlessIn("error:", err)
729         d.addCallback(_check)
730         # Make sure that validation occurs on the to parameter if the
731         # from parameter passes.
732         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
733         d.addCallback(lambda ign: self._create_test_file())
734         d.addCallback(lambda ign: self.do_cli("put", self.datafile,
735                                               "havasu:from"))
736         d.addCallback(lambda ign: self.do_cli("ln", "havasu:from", "huron:to"))
737         d.addCallback(_check)
738         return d
739
740
741 class Put(GridTestMixin, CLITestMixin, unittest.TestCase):
742
743     def test_unlinked_immutable_stdin(self):
744         # tahoe get `echo DATA | tahoe put`
745         # tahoe get `echo DATA | tahoe put -`
746         self.basedir = "cli/Put/unlinked_immutable_stdin"
747         DATA = "data" * 100
748         self.set_up_grid()
749         d = self.do_cli("put", stdin=DATA)
750         def _uploaded(res):
751             (rc, out, err) = res
752             self.failUnlessIn("waiting for file data on stdin..", err)
753             self.failUnlessIn("200 OK", err)
754             self.readcap = out
755             self.failUnless(self.readcap.startswith("URI:CHK:"))
756         d.addCallback(_uploaded)
757         d.addCallback(lambda res: self.do_cli("get", self.readcap))
758         def _downloaded(res):
759             (rc, out, err) = res
760             self.failUnlessReallyEqual(err, "")
761             self.failUnlessReallyEqual(out, DATA)
762         d.addCallback(_downloaded)
763         d.addCallback(lambda res: self.do_cli("put", "-", stdin=DATA))
764         d.addCallback(lambda (rc, out, err):
765                       self.failUnlessReallyEqual(out, self.readcap))
766         return d
767
768     def test_unlinked_immutable_from_file(self):
769         # tahoe put file.txt
770         # tahoe put ./file.txt
771         # tahoe put /tmp/file.txt
772         # tahoe put ~/file.txt
773         self.basedir = "cli/Put/unlinked_immutable_from_file"
774         self.set_up_grid()
775
776         rel_fn = os.path.join(self.basedir, "DATAFILE")
777         abs_fn = unicode_to_argv(abspath_expanduser_unicode(unicode(rel_fn)))
778         # we make the file small enough to fit in a LIT file, for speed
779         fileutil.write(rel_fn, "short file")
780         d = self.do_cli("put", rel_fn)
781         def _uploaded((rc, out, err)):
782             readcap = out
783             self.failUnless(readcap.startswith("URI:LIT:"), readcap)
784             self.readcap = readcap
785         d.addCallback(_uploaded)
786         d.addCallback(lambda res: self.do_cli("put", "./" + rel_fn))
787         d.addCallback(lambda (rc,stdout,stderr):
788                       self.failUnlessReallyEqual(stdout, self.readcap))
789         d.addCallback(lambda res: self.do_cli("put", abs_fn))
790         d.addCallback(lambda (rc,stdout,stderr):
791                       self.failUnlessReallyEqual(stdout, self.readcap))
792         # we just have to assume that ~ is handled properly
793         return d
794
795     def test_immutable_from_file(self):
796         # tahoe put file.txt uploaded.txt
797         # tahoe - uploaded.txt
798         # tahoe put file.txt subdir/uploaded.txt
799         # tahoe put file.txt tahoe:uploaded.txt
800         # tahoe put file.txt tahoe:subdir/uploaded.txt
801         # tahoe put file.txt DIRCAP:./uploaded.txt
802         # tahoe put file.txt DIRCAP:./subdir/uploaded.txt
803         self.basedir = "cli/Put/immutable_from_file"
804         self.set_up_grid()
805
806         rel_fn = os.path.join(self.basedir, "DATAFILE")
807         # we make the file small enough to fit in a LIT file, for speed
808         DATA = "short file"
809         DATA2 = "short file two"
810         fileutil.write(rel_fn, DATA)
811
812         d = self.do_cli("create-alias", "tahoe")
813
814         d.addCallback(lambda res:
815                       self.do_cli("put", rel_fn, "uploaded.txt"))
816         def _uploaded((rc, out, err)):
817             readcap = out.strip()
818             self.failUnless(readcap.startswith("URI:LIT:"), readcap)
819             self.failUnlessIn("201 Created", err)
820             self.readcap = readcap
821         d.addCallback(_uploaded)
822         d.addCallback(lambda res:
823                       self.do_cli("get", "tahoe:uploaded.txt"))
824         d.addCallback(lambda (rc,stdout,stderr):
825                       self.failUnlessReallyEqual(stdout, DATA))
826
827         d.addCallback(lambda res:
828                       self.do_cli("put", "-", "uploaded.txt", stdin=DATA2))
829         def _replaced((rc, out, err)):
830             readcap = out.strip()
831             self.failUnless(readcap.startswith("URI:LIT:"), readcap)
832             self.failUnlessIn("200 OK", err)
833         d.addCallback(_replaced)
834
835         d.addCallback(lambda res:
836                       self.do_cli("put", rel_fn, "subdir/uploaded2.txt"))
837         d.addCallback(lambda res: self.do_cli("get", "subdir/uploaded2.txt"))
838         d.addCallback(lambda (rc,stdout,stderr):
839                       self.failUnlessReallyEqual(stdout, DATA))
840
841         d.addCallback(lambda res:
842                       self.do_cli("put", rel_fn, "tahoe:uploaded3.txt"))
843         d.addCallback(lambda res: self.do_cli("get", "tahoe:uploaded3.txt"))
844         d.addCallback(lambda (rc,stdout,stderr):
845                       self.failUnlessReallyEqual(stdout, DATA))
846
847         d.addCallback(lambda res:
848                       self.do_cli("put", rel_fn, "tahoe:subdir/uploaded4.txt"))
849         d.addCallback(lambda res:
850                       self.do_cli("get", "tahoe:subdir/uploaded4.txt"))
851         d.addCallback(lambda (rc,stdout,stderr):
852                       self.failUnlessReallyEqual(stdout, DATA))
853
854         def _get_dircap(res):
855             self.dircap = get_aliases(self.get_clientdir())["tahoe"]
856         d.addCallback(_get_dircap)
857
858         d.addCallback(lambda res:
859                       self.do_cli("put", rel_fn,
860                                   self.dircap+":./uploaded5.txt"))
861         d.addCallback(lambda res:
862                       self.do_cli("get", "tahoe:uploaded5.txt"))
863         d.addCallback(lambda (rc,stdout,stderr):
864                       self.failUnlessReallyEqual(stdout, DATA))
865
866         d.addCallback(lambda res:
867                       self.do_cli("put", rel_fn,
868                                   self.dircap+":./subdir/uploaded6.txt"))
869         d.addCallback(lambda res:
870                       self.do_cli("get", "tahoe:subdir/uploaded6.txt"))
871         d.addCallback(lambda (rc,stdout,stderr):
872                       self.failUnlessReallyEqual(stdout, DATA))
873
874         return d
875
876     def test_mutable_unlinked(self):
877         # FILECAP = `echo DATA | tahoe put --mutable`
878         # tahoe get FILECAP, compare against DATA
879         # echo DATA2 | tahoe put - FILECAP
880         # tahoe get FILECAP, compare against DATA2
881         # tahoe put file.txt FILECAP
882         self.basedir = "cli/Put/mutable_unlinked"
883         self.set_up_grid()
884
885         DATA = "data" * 100
886         DATA2 = "two" * 100
887         rel_fn = os.path.join(self.basedir, "DATAFILE")
888         DATA3 = "three" * 100
889         fileutil.write(rel_fn, DATA3)
890
891         d = self.do_cli("put", "--mutable", stdin=DATA)
892         def _created(res):
893             (rc, out, err) = res
894             self.failUnlessIn("waiting for file data on stdin..", err)
895             self.failUnlessIn("200 OK", err)
896             self.filecap = out
897             self.failUnless(self.filecap.startswith("URI:SSK:"), self.filecap)
898         d.addCallback(_created)
899         d.addCallback(lambda res: self.do_cli("get", self.filecap))
900         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA))
901
902         d.addCallback(lambda res: self.do_cli("put", "-", self.filecap, stdin=DATA2))
903         def _replaced(res):
904             (rc, out, err) = res
905             self.failUnlessIn("waiting for file data on stdin..", err)
906             self.failUnlessIn("200 OK", err)
907             self.failUnlessReallyEqual(self.filecap, out)
908         d.addCallback(_replaced)
909         d.addCallback(lambda res: self.do_cli("get", self.filecap))
910         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA2))
911
912         d.addCallback(lambda res: self.do_cli("put", rel_fn, self.filecap))
913         def _replaced2(res):
914             (rc, out, err) = res
915             self.failUnlessIn("200 OK", err)
916             self.failUnlessReallyEqual(self.filecap, out)
917         d.addCallback(_replaced2)
918         d.addCallback(lambda res: self.do_cli("get", self.filecap))
919         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA3))
920
921         return d
922
923     def test_mutable(self):
924         # echo DATA1 | tahoe put --mutable - uploaded.txt
925         # echo DATA2 | tahoe put - uploaded.txt # should modify-in-place
926         # tahoe get uploaded.txt, compare against DATA2
927
928         self.basedir = "cli/Put/mutable"
929         self.set_up_grid()
930
931         DATA1 = "data" * 100
932         fn1 = os.path.join(self.basedir, "DATA1")
933         fileutil.write(fn1, DATA1)
934         DATA2 = "two" * 100
935         fn2 = os.path.join(self.basedir, "DATA2")
936         fileutil.write(fn2, DATA2)
937
938         d = self.do_cli("create-alias", "tahoe")
939         d.addCallback(lambda res:
940                       self.do_cli("put", "--mutable", fn1, "tahoe:uploaded.txt"))
941         d.addCallback(lambda res:
942                       self.do_cli("put", fn2, "tahoe:uploaded.txt"))
943         d.addCallback(lambda res:
944                       self.do_cli("get", "tahoe:uploaded.txt"))
945         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA2))
946         return d
947
948     def test_put_with_nonexistent_alias(self):
949         # when invoked with an alias that doesn't exist, 'tahoe put'
950         # should output a useful error message, not a stack trace
951         self.basedir = "cli/Put/put_with_nonexistent_alias"
952         self.set_up_grid()
953         d = self.do_cli("put", "somefile", "fake:afile")
954         def _check((rc, out, err)):
955             self.failUnlessReallyEqual(rc, 1)
956             self.failUnlessIn("error:", err)
957             self.failUnlessReallyEqual(out, "")
958         d.addCallback(_check)
959         return d
960
961     def test_immutable_from_file_unicode(self):
962         # tahoe put "\u00E0 trier.txt" "\u00E0 trier.txt"
963
964         try:
965             a_trier_arg = u"\u00E0 trier.txt".encode(get_argv_encoding())
966         except UnicodeEncodeError:
967             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
968
969         self.skip_if_cannot_represent_filename(u"\u00E0 trier.txt")
970
971         self.basedir = "cli/Put/immutable_from_file_unicode"
972         self.set_up_grid()
973
974         rel_fn = os.path.join(unicode(self.basedir), u"\u00E0 trier.txt")
975         # we make the file small enough to fit in a LIT file, for speed
976         DATA = "short file"
977         fileutil.write(rel_fn, DATA)
978
979         d = self.do_cli("create-alias", "tahoe")
980
981         d.addCallback(lambda res:
982                       self.do_cli("put", rel_fn.encode(get_argv_encoding()), a_trier_arg))
983         def _uploaded((rc, out, err)):
984             readcap = out.strip()
985             self.failUnless(readcap.startswith("URI:LIT:"), readcap)
986             self.failUnlessIn("201 Created", err)
987             self.readcap = readcap
988         d.addCallback(_uploaded)
989
990         d.addCallback(lambda res:
991                       self.do_cli("get", "tahoe:" + a_trier_arg))
992         d.addCallback(lambda (rc, out, err):
993                       self.failUnlessReallyEqual(out, DATA))
994
995         return d
996
997 class List(GridTestMixin, CLITestMixin, unittest.TestCase):
998     def test_list(self):
999         self.basedir = "cli/List/list"
1000         self.set_up_grid()
1001         c0 = self.g.clients[0]
1002         small = "small"
1003
1004         # u"g\u00F6\u00F6d" might not be representable in the argv and/or output encodings.
1005         # It is initially included in the directory in any case.
1006         try:
1007             good_arg = u"g\u00F6\u00F6d".encode(get_argv_encoding())
1008         except UnicodeEncodeError:
1009             good_arg = None
1010
1011         try:
1012             good_out = u"g\u00F6\u00F6d".encode(get_output_encoding())
1013         except UnicodeEncodeError:
1014             good_out = None
1015
1016         d = c0.create_dirnode()
1017         def _stash_root_and_create_file(n):
1018             self.rootnode = n
1019             self.rooturi = n.get_uri()
1020             return n.add_file(u"g\u00F6\u00F6d", upload.Data(small, convergence=""))
1021         d.addCallback(_stash_root_and_create_file)
1022         def _stash_goodcap(n):
1023             self.goodcap = n.get_uri()
1024         d.addCallback(_stash_goodcap)
1025         d.addCallback(lambda ign: self.rootnode.create_subdirectory(u"1share"))
1026         d.addCallback(lambda n:
1027                       self.delete_shares_numbered(n.get_uri(), range(1,10)))
1028         d.addCallback(lambda ign: self.rootnode.create_subdirectory(u"0share"))
1029         d.addCallback(lambda n:
1030                       self.delete_shares_numbered(n.get_uri(), range(0,10)))
1031         d.addCallback(lambda ign:
1032                       self.do_cli("add-alias", "tahoe", self.rooturi))
1033         d.addCallback(lambda ign: self.do_cli("ls"))
1034         def _check1((rc,out,err)):
1035             if good_out is None:
1036                 self.failUnlessReallyEqual(rc, 1)
1037                 self.failUnlessIn("files whose names could not be converted", err)
1038                 self.failUnlessIn(quote_output(u"g\u00F6\u00F6d"), err)
1039                 self.failUnlessReallyEqual(sorted(out.splitlines()), sorted(["0share", "1share"]))
1040             else:
1041                 self.failUnlessReallyEqual(rc, 0)
1042                 self.failUnlessReallyEqual(err, "")
1043                 self.failUnlessReallyEqual(sorted(out.splitlines()), sorted(["0share", "1share", good_out]))
1044         d.addCallback(_check1)
1045         d.addCallback(lambda ign: self.do_cli("ls", "missing"))
1046         def _check2((rc,out,err)):
1047             self.failIfEqual(rc, 0)
1048             self.failUnlessReallyEqual(err.strip(), "No such file or directory")
1049             self.failUnlessReallyEqual(out, "")
1050         d.addCallback(_check2)
1051         d.addCallback(lambda ign: self.do_cli("ls", "1share"))
1052         def _check3((rc,out,err)):
1053             self.failIfEqual(rc, 0)
1054             self.failUnlessIn("Error during GET: 410 Gone", err)
1055             self.failUnlessIn("UnrecoverableFileError:", err)
1056             self.failUnlessIn("could not be retrieved, because there were "
1057                               "insufficient good shares.", err)
1058             self.failUnlessReallyEqual(out, "")
1059         d.addCallback(_check3)
1060         d.addCallback(lambda ign: self.do_cli("ls", "0share"))
1061         d.addCallback(_check3)
1062         def _check4((rc, out, err)):
1063             if good_out is None:
1064                 self.failUnlessReallyEqual(rc, 1)
1065                 self.failUnlessIn("files whose names could not be converted", err)
1066                 self.failUnlessIn(quote_output(u"g\u00F6\u00F6d"), err)
1067                 self.failUnlessReallyEqual(out, "")
1068             else:
1069                 # listing a file (as dir/filename) should have the edge metadata,
1070                 # including the filename
1071                 self.failUnlessReallyEqual(rc, 0)
1072                 self.failUnlessIn(good_out, out)
1073                 self.failIfIn("-r-- %d -" % len(small), out,
1074                               "trailing hyphen means unknown date")
1075
1076         if good_arg is not None:
1077             d.addCallback(lambda ign: self.do_cli("ls", "-l", good_arg))
1078             d.addCallback(_check4)
1079             # listing a file as $DIRCAP/filename should work just like dir/filename
1080             d.addCallback(lambda ign: self.do_cli("ls", "-l", self.rooturi + "/" + good_arg))
1081             d.addCallback(_check4)
1082             # and similarly for $DIRCAP:./filename
1083             d.addCallback(lambda ign: self.do_cli("ls", "-l", self.rooturi + ":./" + good_arg))
1084             d.addCallback(_check4)
1085
1086         def _check5((rc, out, err)):
1087             # listing a raw filecap should not explode, but it will have no
1088             # metadata, just the size
1089             self.failUnlessReallyEqual(rc, 0)
1090             self.failUnlessReallyEqual("-r-- %d -" % len(small), out.strip())
1091         d.addCallback(lambda ign: self.do_cli("ls", "-l", self.goodcap))
1092         d.addCallback(_check5)
1093
1094         # Now rename 'g\u00F6\u00F6d' to 'good' and repeat the tests that might have been skipped due
1095         # to encoding problems.
1096         d.addCallback(lambda ign: self.rootnode.move_child_to(u"g\u00F6\u00F6d", self.rootnode, u"good"))
1097
1098         d.addCallback(lambda ign: self.do_cli("ls"))
1099         def _check1_ascii((rc,out,err)):
1100             self.failUnlessReallyEqual(rc, 0)
1101             self.failUnlessReallyEqual(err, "")
1102             self.failUnlessReallyEqual(sorted(out.splitlines()), sorted(["0share", "1share", "good"]))
1103         d.addCallback(_check1_ascii)
1104         def _check4_ascii((rc, out, err)):
1105             # listing a file (as dir/filename) should have the edge metadata,
1106             # including the filename
1107             self.failUnlessReallyEqual(rc, 0)
1108             self.failUnlessIn("good", out)
1109             self.failIfIn("-r-- %d -" % len(small), out,
1110                           "trailing hyphen means unknown date")
1111
1112         d.addCallback(lambda ign: self.do_cli("ls", "-l", "good"))
1113         d.addCallback(_check4_ascii)
1114         # listing a file as $DIRCAP/filename should work just like dir/filename
1115         d.addCallback(lambda ign: self.do_cli("ls", "-l", self.rooturi + "/good"))
1116         d.addCallback(_check4_ascii)
1117         # and similarly for $DIRCAP:./filename
1118         d.addCallback(lambda ign: self.do_cli("ls", "-l", self.rooturi + ":./good"))
1119         d.addCallback(_check4_ascii)
1120
1121         unknown_immcap = "imm.URI:unknown"
1122         def _create_unknown(ign):
1123             nm = c0.nodemaker
1124             kids = {u"unknownchild-imm": (nm.create_from_cap(unknown_immcap), {})}
1125             return self.rootnode.create_subdirectory(u"unknown", initial_children=kids,
1126                                                      mutable=False)
1127         d.addCallback(_create_unknown)
1128         def _check6((rc, out, err)):
1129             # listing a directory referencing an unknown object should print
1130             # an extra message to stderr
1131             self.failUnlessReallyEqual(rc, 0)
1132             self.failUnlessIn("?r-- ? - unknownchild-imm\n", out)
1133             self.failUnlessIn("included unknown objects", err)
1134         d.addCallback(lambda ign: self.do_cli("ls", "-l", "unknown"))
1135         d.addCallback(_check6)
1136         def _check7((rc, out, err)):
1137             # listing an unknown cap directly should print an extra message
1138             # to stderr (currently this only works if the URI starts with 'URI:'
1139             # after any 'ro.' or 'imm.' prefix, otherwise it will be confused
1140             # with an alias).
1141             self.failUnlessReallyEqual(rc, 0)
1142             self.failUnlessIn("?r-- ? -\n", out)
1143             self.failUnlessIn("included unknown objects", err)
1144         d.addCallback(lambda ign: self.do_cli("ls", "-l", unknown_immcap))
1145         d.addCallback(_check7)
1146         return d
1147
1148     def test_list_without_alias(self):
1149         # doing just 'tahoe ls' without specifying an alias or first
1150         # doing 'tahoe create-alias tahoe' should fail gracefully.
1151         self.basedir = "cli/List/list_without_alias"
1152         self.set_up_grid()
1153         d = self.do_cli("ls")
1154         def _check((rc, out, err)):
1155             self.failUnlessReallyEqual(rc, 1)
1156             self.failUnlessIn("error:", err)
1157             self.failUnlessReallyEqual(out, "")
1158         d.addCallback(_check)
1159         return d
1160
1161     def test_list_with_nonexistent_alias(self):
1162         # doing 'tahoe ls' while specifying an alias that doesn't already
1163         # exist should fail with an informative error message
1164         self.basedir = "cli/List/list_with_nonexistent_alias"
1165         self.set_up_grid()
1166         d = self.do_cli("ls", "nonexistent:")
1167         def _check((rc, out, err)):
1168             self.failUnlessReallyEqual(rc, 1)
1169             self.failUnlessIn("error:", err)
1170             self.failUnlessIn("nonexistent", err)
1171             self.failUnlessReallyEqual(out, "")
1172         d.addCallback(_check)
1173         return d
1174
1175
1176 class Mv(GridTestMixin, CLITestMixin, unittest.TestCase):
1177     def test_mv_behavior(self):
1178         self.basedir = "cli/Mv/mv_behavior"
1179         self.set_up_grid()
1180         fn1 = os.path.join(self.basedir, "file1")
1181         DATA1 = "Nuclear launch codes"
1182         fileutil.write(fn1, DATA1)
1183         fn2 = os.path.join(self.basedir, "file2")
1184         DATA2 = "UML diagrams"
1185         fileutil.write(fn2, DATA2)
1186         # copy both files to the grid
1187         d = self.do_cli("create-alias", "tahoe")
1188         d.addCallback(lambda res:
1189             self.do_cli("cp", fn1, "tahoe:"))
1190         d.addCallback(lambda res:
1191             self.do_cli("cp", fn2, "tahoe:"))
1192
1193         # do mv file1 file3
1194         # (we should be able to rename files)
1195         d.addCallback(lambda res:
1196             self.do_cli("mv", "tahoe:file1", "tahoe:file3"))
1197         d.addCallback(lambda (rc, out, err):
1198             self.failUnlessIn("OK", out, "mv didn't rename a file"))
1199
1200         # do mv file3 file2
1201         # (This should succeed without issue)
1202         d.addCallback(lambda res:
1203             self.do_cli("mv", "tahoe:file3", "tahoe:file2"))
1204         # Out should contain "OK" to show that the transfer worked.
1205         d.addCallback(lambda (rc,out,err):
1206             self.failUnlessIn("OK", out, "mv didn't output OK after mving"))
1207
1208         # Next, make a remote directory.
1209         d.addCallback(lambda res:
1210             self.do_cli("mkdir", "tahoe:directory"))
1211
1212         # mv file2 directory
1213         # (should fail with a descriptive error message; the CLI mv
1214         #  client should support this)
1215         d.addCallback(lambda res:
1216             self.do_cli("mv", "tahoe:file2", "tahoe:directory"))
1217         d.addCallback(lambda (rc, out, err):
1218             self.failUnlessIn(
1219                 "Error: You can't overwrite a directory with a file", err,
1220                 "mv shouldn't overwrite directories" ))
1221
1222         # mv file2 directory/
1223         # (should succeed by making file2 a child node of directory)
1224         d.addCallback(lambda res:
1225             self.do_cli("mv", "tahoe:file2", "tahoe:directory/"))
1226         # We should see an "OK"...
1227         d.addCallback(lambda (rc, out, err):
1228             self.failUnlessIn("OK", out,
1229                             "mv didn't mv a file into a directory"))
1230         # ... and be able to GET the file
1231         d.addCallback(lambda res:
1232             self.do_cli("get", "tahoe:directory/file2", self.basedir + "new"))
1233         d.addCallback(lambda (rc, out, err):
1234             self.failUnless(os.path.exists(self.basedir + "new"),
1235                             "mv didn't write the destination file"))
1236         # ... and not find the file where it was before.
1237         d.addCallback(lambda res:
1238             self.do_cli("get", "tahoe:file2", "file2"))
1239         d.addCallback(lambda (rc, out, err):
1240             self.failUnlessIn("404", err,
1241                             "mv left the source file intact"))
1242
1243         # Let's build:
1244         # directory/directory2/some_file
1245         # directory3
1246         d.addCallback(lambda res:
1247             self.do_cli("mkdir", "tahoe:directory/directory2"))
1248         d.addCallback(lambda res:
1249             self.do_cli("cp", fn2, "tahoe:directory/directory2/some_file"))
1250         d.addCallback(lambda res:
1251             self.do_cli("mkdir", "tahoe:directory3"))
1252
1253         # Let's now try to mv directory/directory2/some_file to
1254         # directory3/some_file
1255         d.addCallback(lambda res:
1256             self.do_cli("mv", "tahoe:directory/directory2/some_file",
1257                         "tahoe:directory3/"))
1258         # We should have just some_file in tahoe:directory3
1259         d.addCallback(lambda res:
1260             self.do_cli("get", "tahoe:directory3/some_file", "some_file"))
1261         d.addCallback(lambda (rc, out, err):
1262             self.failUnless("404" not in err,
1263                               "mv didn't handle nested directories correctly"))
1264         d.addCallback(lambda res:
1265             self.do_cli("get", "tahoe:directory3/directory", "directory"))
1266         d.addCallback(lambda (rc, out, err):
1267             self.failUnlessIn("404", err,
1268                               "mv moved the wrong thing"))
1269         return d
1270
1271     def test_mv_without_alias(self):
1272         # doing 'tahoe mv' without explicitly specifying an alias or
1273         # creating the default 'tahoe' alias should fail with a useful
1274         # error message.
1275         self.basedir = "cli/Mv/mv_without_alias"
1276         self.set_up_grid()
1277         d = self.do_cli("mv", "afile", "anotherfile")
1278         def _check((rc, out, err)):
1279             self.failUnlessReallyEqual(rc, 1)
1280             self.failUnlessIn("error:", err)
1281             self.failUnlessReallyEqual(out, "")
1282         d.addCallback(_check)
1283         # check to see that the validation extends to the
1284         # target argument by making an alias that will work with the first
1285         # one.
1286         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
1287         def _create_a_test_file(ign):
1288             self.test_file_path = os.path.join(self.basedir, "afile")
1289             fileutil.write(self.test_file_path, "puppies" * 100)
1290         d.addCallback(_create_a_test_file)
1291         d.addCallback(lambda ign: self.do_cli("put", self.test_file_path,
1292                                               "havasu:afile"))
1293         d.addCallback(lambda ign: self.do_cli("mv", "havasu:afile",
1294                                               "anotherfile"))
1295         d.addCallback(_check)
1296         return d
1297
1298     def test_mv_with_nonexistent_alias(self):
1299         # doing 'tahoe mv' with an alias that doesn't exist should fail
1300         # with an informative error message.
1301         self.basedir = "cli/Mv/mv_with_nonexistent_alias"
1302         self.set_up_grid()
1303         d = self.do_cli("mv", "fake:afile", "fake:anotherfile")
1304         def _check((rc, out, err)):
1305             self.failUnlessReallyEqual(rc, 1)
1306             self.failUnlessIn("error:", err)
1307             self.failUnlessIn("fake", err)
1308             self.failUnlessReallyEqual(out, "")
1309         d.addCallback(_check)
1310         # check to see that the validation extends to the
1311         # target argument by making an alias that will work with the first
1312         # one.
1313         d.addCallback(lambda ign: self.do_cli("create-alias", "havasu"))
1314         def _create_a_test_file(ign):
1315             self.test_file_path = os.path.join(self.basedir, "afile")
1316             fileutil.write(self.test_file_path, "puppies" * 100)
1317         d.addCallback(_create_a_test_file)
1318         d.addCallback(lambda ign: self.do_cli("put", self.test_file_path,
1319                                               "havasu:afile"))
1320         d.addCallback(lambda ign: self.do_cli("mv", "havasu:afile",
1321                                               "fake:anotherfile"))
1322         d.addCallback(_check)
1323         return d
1324
1325
1326 class Cp(GridTestMixin, CLITestMixin, unittest.TestCase):
1327
1328     def test_not_enough_args(self):
1329         o = cli.CpOptions()
1330         self.failUnlessRaises(usage.UsageError,
1331                               o.parseOptions, ["onearg"])
1332
1333     def test_unicode_filename(self):
1334         self.basedir = "cli/Cp/unicode_filename"
1335
1336         fn1 = os.path.join(unicode(self.basedir), u"\u00C4rtonwall")
1337         try:
1338             fn1_arg = fn1.encode(get_argv_encoding())
1339             artonwall_arg = u"\u00C4rtonwall".encode(get_argv_encoding())
1340         except UnicodeEncodeError:
1341             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
1342
1343         self.skip_if_cannot_represent_filename(fn1)
1344
1345         self.set_up_grid()
1346
1347         DATA1 = "unicode file content"
1348         fileutil.write(fn1, DATA1)
1349
1350         fn2 = os.path.join(self.basedir, "Metallica")
1351         DATA2 = "non-unicode file content"
1352         fileutil.write(fn2, DATA2)
1353
1354         d = self.do_cli("create-alias", "tahoe")
1355
1356         d.addCallback(lambda res: self.do_cli("cp", fn1_arg, "tahoe:"))
1357
1358         d.addCallback(lambda res: self.do_cli("get", "tahoe:" + artonwall_arg))
1359         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA1))
1360
1361         d.addCallback(lambda res: self.do_cli("cp", fn2, "tahoe:"))
1362
1363         d.addCallback(lambda res: self.do_cli("get", "tahoe:Metallica"))
1364         d.addCallback(lambda (rc,out,err): self.failUnlessReallyEqual(out, DATA2))
1365
1366         d.addCallback(lambda res: self.do_cli("ls", "tahoe:"))
1367         def _check((rc, out, err)):
1368             try:
1369                 unicode_to_output(u"\u00C4rtonwall")
1370             except UnicodeEncodeError:
1371                 self.failUnlessReallyEqual(rc, 1)
1372                 self.failUnlessReallyEqual(out, "Metallica\n")
1373                 self.failUnlessIn(quote_output(u"\u00C4rtonwall"), err)
1374                 self.failUnlessIn("files whose names could not be converted", err)
1375             else:
1376                 self.failUnlessReallyEqual(rc, 0)
1377                 self.failUnlessReallyEqual(out.decode(get_output_encoding()), u"Metallica\n\u00C4rtonwall\n")
1378                 self.failUnlessReallyEqual(err, "")
1379         d.addCallback(_check)
1380
1381         return d
1382
1383     def test_dangling_symlink_vs_recursion(self):
1384         if not hasattr(os, 'symlink'):
1385             raise unittest.SkipTest("Symlinks are not supported by Python on this platform.")
1386
1387         # cp -r on a directory containing a dangling symlink shouldn't assert
1388         self.basedir = "cli/Cp/dangling_symlink_vs_recursion"
1389         self.set_up_grid()
1390         dn = os.path.join(self.basedir, "dir")
1391         os.mkdir(dn)
1392         fn = os.path.join(dn, "Fakebandica")
1393         ln = os.path.join(dn, "link")
1394         os.symlink(fn, ln)
1395
1396         d = self.do_cli("create-alias", "tahoe")
1397         d.addCallback(lambda res: self.do_cli("cp", "--recursive",
1398                                               dn, "tahoe:"))
1399         return d
1400
1401     def test_copy_using_filecap(self):
1402         self.basedir = "cli/Cp/test_copy_using_filecap"
1403         self.set_up_grid()
1404         outdir = os.path.join(self.basedir, "outdir")
1405         os.mkdir(outdir)
1406         fn1 = os.path.join(self.basedir, "Metallica")
1407         fn2 = os.path.join(outdir, "Not Metallica")
1408         fn3 = os.path.join(outdir, "test2")
1409         DATA1 = "puppies" * 10000
1410         fileutil.write(fn1, DATA1)
1411
1412         d = self.do_cli("create-alias", "tahoe")
1413         d.addCallback(lambda ign: self.do_cli("put", fn1))
1414         def _put_file((rc, out, err)):
1415             self.failUnlessReallyEqual(rc, 0)
1416             self.failUnlessIn("200 OK", err)
1417             # keep track of the filecap
1418             self.filecap = out.strip()
1419         d.addCallback(_put_file)
1420
1421         # Let's try copying this to the disk using the filecap
1422         #  cp FILECAP filename
1423         d.addCallback(lambda ign: self.do_cli("cp", self.filecap, fn2))
1424         def _copy_file((rc, out, err)):
1425             self.failUnlessReallyEqual(rc, 0)
1426             results = fileutil.read(fn2)
1427             self.failUnlessReallyEqual(results, DATA1)
1428         d.addCallback(_copy_file)
1429
1430         # Test with ./ (see #761)
1431         #  cp FILECAP localdir
1432         d.addCallback(lambda ign: self.do_cli("cp", self.filecap, outdir))
1433         def _resp((rc, out, err)):
1434             self.failUnlessReallyEqual(rc, 1)
1435             self.failUnlessIn("error: you must specify a destination filename",
1436                               err)
1437             self.failUnlessReallyEqual(out, "")
1438         d.addCallback(_resp)
1439
1440         # Create a directory, linked at tahoe:test
1441         d.addCallback(lambda ign: self.do_cli("mkdir", "tahoe:test"))
1442         def _get_dir((rc, out, err)):
1443             self.failUnlessReallyEqual(rc, 0)
1444             self.dircap = out.strip()
1445         d.addCallback(_get_dir)
1446
1447         # Upload a file to the directory
1448         d.addCallback(lambda ign:
1449                       self.do_cli("put", fn1, "tahoe:test/test_file"))
1450         d.addCallback(lambda (rc, out, err): self.failUnlessReallyEqual(rc, 0))
1451
1452         #  cp DIRCAP/filename localdir
1453         d.addCallback(lambda ign:
1454                       self.do_cli("cp",  self.dircap + "/test_file", outdir))
1455         def _get_resp((rc, out, err)):
1456             self.failUnlessReallyEqual(rc, 0)
1457             results = fileutil.read(os.path.join(outdir, "test_file"))
1458             self.failUnlessReallyEqual(results, DATA1)
1459         d.addCallback(_get_resp)
1460
1461         #  cp -r DIRCAP/filename filename2
1462         d.addCallback(lambda ign:
1463                       self.do_cli("cp",  self.dircap + "/test_file", fn3))
1464         def _get_resp2((rc, out, err)):
1465             self.failUnlessReallyEqual(rc, 0)
1466             results = fileutil.read(fn3)
1467             self.failUnlessReallyEqual(results, DATA1)
1468         d.addCallback(_get_resp2)
1469         return d
1470
1471     def test_cp_with_nonexistent_alias(self):
1472         # when invoked with an alias or aliases that don't exist, 'tahoe cp'
1473         # should output a sensible error message rather than a stack trace.
1474         self.basedir = "cli/Cp/cp_with_nonexistent_alias"
1475         self.set_up_grid()
1476         d = self.do_cli("cp", "fake:file1", "fake:file2")
1477         def _check((rc, out, err)):
1478             self.failUnlessReallyEqual(rc, 1)
1479             self.failUnlessIn("error:", err)
1480         d.addCallback(_check)
1481         # 'tahoe cp' actually processes the target argument first, so we need
1482         # to check to make sure that validation extends to the source
1483         # argument.
1484         d.addCallback(lambda ign: self.do_cli("create-alias", "tahoe"))
1485         d.addCallback(lambda ign: self.do_cli("cp", "fake:file1",
1486                                               "tahoe:file2"))
1487         d.addCallback(_check)
1488         return d
1489
1490
1491 class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
1492
1493     def writeto(self, path, data):
1494         full_path = os.path.join(self.basedir, "home", path)
1495         fileutil.make_dirs(os.path.dirname(full_path))
1496         fileutil.write(full_path, data)
1497
1498     def count_output(self, out):
1499         mo = re.search(r"(\d)+ files uploaded \((\d+) reused\), "
1500                         "(\d)+ files skipped, "
1501                         "(\d+) directories created \((\d+) reused\), "
1502                         "(\d+) directories skipped", out)
1503         return [int(s) for s in mo.groups()]
1504
1505     def count_output2(self, out):
1506         mo = re.search(r"(\d)+ files checked, (\d+) directories checked", out)
1507         return [int(s) for s in mo.groups()]
1508
1509     def test_backup(self):
1510         self.basedir = "cli/Backup/backup"
1511         self.set_up_grid()
1512
1513         # is the backupdb available? If so, we test that a second backup does
1514         # not create new directories.
1515         hush = StringIO()
1516         have_bdb = backupdb.get_backupdb(os.path.join(self.basedir, "dbtest"),
1517                                          hush)
1518
1519         # create a small local directory with a couple of files
1520         source = os.path.join(self.basedir, "home")
1521         fileutil.make_dirs(os.path.join(source, "empty"))
1522         self.writeto("parent/subdir/foo.txt", "foo")
1523         self.writeto("parent/subdir/bar.txt", "bar\n" * 1000)
1524         self.writeto("parent/blah.txt", "blah")
1525
1526         def do_backup(verbose=False):
1527             cmd = ["backup"]
1528             if verbose:
1529                 cmd.append("--verbose")
1530             cmd.append(source)
1531             cmd.append("tahoe:backups")
1532             return self.do_cli(*cmd)
1533
1534         d = self.do_cli("create-alias", "tahoe")
1535
1536         if not have_bdb:
1537             d.addCallback(lambda res: self.do_cli("backup", source, "tahoe:backups"))
1538             def _should_complain((rc, out, err)):
1539                 self.failUnless("I was unable to import a python sqlite library" in err, err)
1540             d.addCallback(_should_complain)
1541             d.addCallback(self.stall, 1.1) # make sure the backups get distinct timestamps
1542
1543         d.addCallback(lambda res: do_backup())
1544         def _check0((rc, out, err)):
1545             self.failUnlessReallyEqual(err, "")
1546             self.failUnlessReallyEqual(rc, 0)
1547             fu, fr, fs, dc, dr, ds = self.count_output(out)
1548             # foo.txt, bar.txt, blah.txt
1549             self.failUnlessReallyEqual(fu, 3)
1550             self.failUnlessReallyEqual(fr, 0)
1551             self.failUnlessReallyEqual(fs, 0)
1552             # empty, home, home/parent, home/parent/subdir
1553             self.failUnlessReallyEqual(dc, 4)
1554             self.failUnlessReallyEqual(dr, 0)
1555             self.failUnlessReallyEqual(ds, 0)
1556         d.addCallback(_check0)
1557
1558         d.addCallback(lambda res: self.do_cli("ls", "--uri", "tahoe:backups"))
1559         def _check1((rc, out, err)):
1560             self.failUnlessReallyEqual(err, "")
1561             self.failUnlessReallyEqual(rc, 0)
1562             lines = out.split("\n")
1563             children = dict([line.split() for line in lines if line])
1564             latest_uri = children["Latest"]
1565             self.failUnless(latest_uri.startswith("URI:DIR2-CHK:"), latest_uri)
1566             childnames = children.keys()
1567             self.failUnlessReallyEqual(sorted(childnames), ["Archives", "Latest"])
1568         d.addCallback(_check1)
1569         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Latest"))
1570         def _check2((rc, out, err)):
1571             self.failUnlessReallyEqual(err, "")
1572             self.failUnlessReallyEqual(rc, 0)
1573             self.failUnlessReallyEqual(sorted(out.split()), ["empty", "parent"])
1574         d.addCallback(_check2)
1575         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Latest/empty"))
1576         def _check2a((rc, out, err)):
1577             self.failUnlessReallyEqual(err, "")
1578             self.failUnlessReallyEqual(rc, 0)
1579             self.failUnlessReallyEqual(out.strip(), "")
1580         d.addCallback(_check2a)
1581         d.addCallback(lambda res: self.do_cli("get", "tahoe:backups/Latest/parent/subdir/foo.txt"))
1582         def _check3((rc, out, err)):
1583             self.failUnlessReallyEqual(err, "")
1584             self.failUnlessReallyEqual(rc, 0)
1585             self.failUnlessReallyEqual(out, "foo")
1586         d.addCallback(_check3)
1587         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
1588         def _check4((rc, out, err)):
1589             self.failUnlessReallyEqual(err, "")
1590             self.failUnlessReallyEqual(rc, 0)
1591             self.old_archives = out.split()
1592             self.failUnlessReallyEqual(len(self.old_archives), 1)
1593         d.addCallback(_check4)
1594
1595
1596         d.addCallback(self.stall, 1.1)
1597         d.addCallback(lambda res: do_backup())
1598         def _check4a((rc, out, err)):
1599             # second backup should reuse everything, if the backupdb is
1600             # available
1601             self.failUnlessReallyEqual(err, "")
1602             self.failUnlessReallyEqual(rc, 0)
1603             if have_bdb:
1604                 fu, fr, fs, dc, dr, ds = self.count_output(out)
1605                 # foo.txt, bar.txt, blah.txt
1606                 self.failUnlessReallyEqual(fu, 0)
1607                 self.failUnlessReallyEqual(fr, 3)
1608                 self.failUnlessReallyEqual(fs, 0)
1609                 # empty, home, home/parent, home/parent/subdir
1610                 self.failUnlessReallyEqual(dc, 0)
1611                 self.failUnlessReallyEqual(dr, 4)
1612                 self.failUnlessReallyEqual(ds, 0)
1613         d.addCallback(_check4a)
1614
1615         if have_bdb:
1616             # sneak into the backupdb, crank back the "last checked"
1617             # timestamp to force a check on all files
1618             def _reset_last_checked(res):
1619                 dbfile = os.path.join(self.get_clientdir(),
1620                                       "private", "backupdb.sqlite")
1621                 self.failUnless(os.path.exists(dbfile), dbfile)
1622                 bdb = backupdb.get_backupdb(dbfile)
1623                 bdb.cursor.execute("UPDATE last_upload SET last_checked=0")
1624                 bdb.cursor.execute("UPDATE directories SET last_checked=0")
1625                 bdb.connection.commit()
1626
1627             d.addCallback(_reset_last_checked)
1628
1629             d.addCallback(self.stall, 1.1)
1630             d.addCallback(lambda res: do_backup(verbose=True))
1631             def _check4b((rc, out, err)):
1632                 # we should check all files, and re-use all of them. None of
1633                 # the directories should have been changed, so we should
1634                 # re-use all of them too.
1635                 self.failUnlessReallyEqual(err, "")
1636                 self.failUnlessReallyEqual(rc, 0)
1637                 fu, fr, fs, dc, dr, ds = self.count_output(out)
1638                 fchecked, dchecked = self.count_output2(out)
1639                 self.failUnlessReallyEqual(fchecked, 3)
1640                 self.failUnlessReallyEqual(fu, 0)
1641                 self.failUnlessReallyEqual(fr, 3)
1642                 self.failUnlessReallyEqual(fs, 0)
1643                 self.failUnlessReallyEqual(dchecked, 4)
1644                 self.failUnlessReallyEqual(dc, 0)
1645                 self.failUnlessReallyEqual(dr, 4)
1646                 self.failUnlessReallyEqual(ds, 0)
1647             d.addCallback(_check4b)
1648
1649         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
1650         def _check5((rc, out, err)):
1651             self.failUnlessReallyEqual(err, "")
1652             self.failUnlessReallyEqual(rc, 0)
1653             self.new_archives = out.split()
1654             expected_new = 2
1655             if have_bdb:
1656                 expected_new += 1
1657             self.failUnlessReallyEqual(len(self.new_archives), expected_new, out)
1658             # the original backup should still be the oldest (i.e. sorts
1659             # alphabetically towards the beginning)
1660             self.failUnlessReallyEqual(sorted(self.new_archives)[0],
1661                                  self.old_archives[0])
1662         d.addCallback(_check5)
1663
1664         d.addCallback(self.stall, 1.1)
1665         def _modify(res):
1666             self.writeto("parent/subdir/foo.txt", "FOOF!")
1667             # and turn a file into a directory
1668             os.unlink(os.path.join(source, "parent/blah.txt"))
1669             os.mkdir(os.path.join(source, "parent/blah.txt"))
1670             self.writeto("parent/blah.txt/surprise file", "surprise")
1671             self.writeto("parent/blah.txt/surprisedir/subfile", "surprise")
1672             # turn a directory into a file
1673             os.rmdir(os.path.join(source, "empty"))
1674             self.writeto("empty", "imagine nothing being here")
1675             return do_backup()
1676         d.addCallback(_modify)
1677         def _check5a((rc, out, err)):
1678             # second backup should reuse bar.txt (if backupdb is available),
1679             # and upload the rest. None of the directories can be reused.
1680             self.failUnlessReallyEqual(err, "")
1681             self.failUnlessReallyEqual(rc, 0)
1682             if have_bdb:
1683                 fu, fr, fs, dc, dr, ds = self.count_output(out)
1684                 # new foo.txt, surprise file, subfile, empty
1685                 self.failUnlessReallyEqual(fu, 4)
1686                 # old bar.txt
1687                 self.failUnlessReallyEqual(fr, 1)
1688                 self.failUnlessReallyEqual(fs, 0)
1689                 # home, parent, subdir, blah.txt, surprisedir
1690                 self.failUnlessReallyEqual(dc, 5)
1691                 self.failUnlessReallyEqual(dr, 0)
1692                 self.failUnlessReallyEqual(ds, 0)
1693         d.addCallback(_check5a)
1694         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
1695         def _check6((rc, out, err)):
1696             self.failUnlessReallyEqual(err, "")
1697             self.failUnlessReallyEqual(rc, 0)
1698             self.new_archives = out.split()
1699             expected_new = 3
1700             if have_bdb:
1701                 expected_new += 1
1702             self.failUnlessReallyEqual(len(self.new_archives), expected_new)
1703             self.failUnlessReallyEqual(sorted(self.new_archives)[0],
1704                                  self.old_archives[0])
1705         d.addCallback(_check6)
1706         d.addCallback(lambda res: self.do_cli("get", "tahoe:backups/Latest/parent/subdir/foo.txt"))
1707         def _check7((rc, out, err)):
1708             self.failUnlessReallyEqual(err, "")
1709             self.failUnlessReallyEqual(rc, 0)
1710             self.failUnlessReallyEqual(out, "FOOF!")
1711             # the old snapshot should not be modified
1712             return self.do_cli("get", "tahoe:backups/Archives/%s/parent/subdir/foo.txt" % self.old_archives[0])
1713         d.addCallback(_check7)
1714         def _check8((rc, out, err)):
1715             self.failUnlessReallyEqual(err, "")
1716             self.failUnlessReallyEqual(rc, 0)
1717             self.failUnlessReallyEqual(out, "foo")
1718         d.addCallback(_check8)
1719
1720         return d
1721
1722     # on our old dapper buildslave, this test takes a long time (usually
1723     # 130s), so we have to bump up the default 120s timeout. The create-alias
1724     # and initial backup alone take 60s, probably because of the handful of
1725     # dirnodes being created (RSA key generation). The backup between check4
1726     # and check4a takes 6s, as does the backup before check4b.
1727     test_backup.timeout = 3000
1728
1729     def _check_filtering(self, filtered, all, included, excluded):
1730         filtered = set(filtered)
1731         all = set(all)
1732         included = set(included)
1733         excluded = set(excluded)
1734         self.failUnlessReallyEqual(filtered, included)
1735         self.failUnlessReallyEqual(all.difference(filtered), excluded)
1736
1737     def test_exclude_options(self):
1738         root_listdir = (u'lib.a', u'_darcs', u'subdir', u'nice_doc.lyx')
1739         subdir_listdir = (u'another_doc.lyx', u'run_snake_run.py', u'CVS', u'.svn', u'_darcs')
1740         basedir = "cli/Backup/exclude_options"
1741         fileutil.make_dirs(basedir)
1742         nodeurl_path = os.path.join(basedir, 'node.url')
1743         fileutil.write(nodeurl_path, 'http://example.net:2357/')
1744
1745         # test simple exclude
1746         backup_options = cli.BackupOptions()
1747         backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
1748                                      basedir, 'from', 'to'])
1749         filtered = list(backup_options.filter_listdir(root_listdir))
1750         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
1751                               (u'nice_doc.lyx',))
1752         # multiple exclude
1753         backup_options = cli.BackupOptions()
1754         backup_options.parseOptions(['--exclude', '*lyx', '--exclude', 'lib.?', '--node-directory',
1755                                      basedir, 'from', 'to'])
1756         filtered = list(backup_options.filter_listdir(root_listdir))
1757         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
1758                               (u'nice_doc.lyx', u'lib.a'))
1759         # vcs metadata exclusion
1760         backup_options = cli.BackupOptions()
1761         backup_options.parseOptions(['--exclude-vcs', '--node-directory',
1762                                      basedir, 'from', 'to'])
1763         filtered = list(backup_options.filter_listdir(subdir_listdir))
1764         self._check_filtering(filtered, subdir_listdir, (u'another_doc.lyx', u'run_snake_run.py',),
1765                               (u'CVS', u'.svn', u'_darcs'))
1766         # read exclude patterns from file
1767         exclusion_string = "_darcs\n*py\n.svn"
1768         excl_filepath = os.path.join(basedir, 'exclusion')
1769         fileutil.write(excl_filepath, exclusion_string)
1770         backup_options = cli.BackupOptions()
1771         backup_options.parseOptions(['--exclude-from', excl_filepath, '--node-directory',
1772                                      basedir, 'from', 'to'])
1773         filtered = list(backup_options.filter_listdir(subdir_listdir))
1774         self._check_filtering(filtered, subdir_listdir, (u'another_doc.lyx', u'CVS'),
1775                               (u'.svn', u'_darcs', u'run_snake_run.py'))
1776         # test BackupConfigurationError
1777         self.failUnlessRaises(cli.BackupConfigurationError,
1778                               backup_options.parseOptions,
1779                               ['--exclude-from', excl_filepath + '.no', '--node-directory',
1780                                basedir, 'from', 'to'])
1781
1782         # test that an iterator works too
1783         backup_options = cli.BackupOptions()
1784         backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
1785                                      basedir, 'from', 'to'])
1786         filtered = list(backup_options.filter_listdir(iter(root_listdir)))
1787         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
1788                               (u'nice_doc.lyx',))
1789
1790     def test_exclude_options_unicode(self):
1791         nice_doc = u"nice_d\u00F8c.lyx"
1792         try:
1793             doc_pattern_arg = u"*d\u00F8c*".encode(get_argv_encoding())
1794         except UnicodeEncodeError:
1795             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
1796
1797         root_listdir = (u'lib.a', u'_darcs', u'subdir', nice_doc)
1798         basedir = "cli/Backup/exclude_options_unicode"
1799         fileutil.make_dirs(basedir)
1800         nodeurl_path = os.path.join(basedir, 'node.url')
1801         fileutil.write(nodeurl_path, 'http://example.net:2357/')
1802
1803         # test simple exclude
1804         backup_options = cli.BackupOptions()
1805         backup_options.parseOptions(['--exclude', doc_pattern_arg, '--node-directory',
1806                                      basedir, 'from', 'to'])
1807         filtered = list(backup_options.filter_listdir(root_listdir))
1808         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
1809                               (nice_doc,))
1810         # multiple exclude
1811         backup_options = cli.BackupOptions()
1812         backup_options.parseOptions(['--exclude', doc_pattern_arg, '--exclude', 'lib.?', '--node-directory',
1813                                      basedir, 'from', 'to'])
1814         filtered = list(backup_options.filter_listdir(root_listdir))
1815         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
1816                              (nice_doc, u'lib.a'))
1817         # read exclude patterns from file
1818         exclusion_string = doc_pattern_arg + "\nlib.?"
1819         excl_filepath = os.path.join(basedir, 'exclusion')
1820         fileutil.write(excl_filepath, exclusion_string)
1821         backup_options = cli.BackupOptions()
1822         backup_options.parseOptions(['--exclude-from', excl_filepath, '--node-directory',
1823                                      basedir, 'from', 'to'])
1824         filtered = list(backup_options.filter_listdir(root_listdir))
1825         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
1826                              (nice_doc, u'lib.a'))
1827
1828         # test that an iterator works too
1829         backup_options = cli.BackupOptions()
1830         backup_options.parseOptions(['--exclude', doc_pattern_arg, '--node-directory',
1831                                      basedir, 'from', 'to'])
1832         filtered = list(backup_options.filter_listdir(iter(root_listdir)))
1833         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
1834                               (nice_doc,))
1835
1836     def test_ignore_symlinks(self):
1837         if not hasattr(os, 'symlink'):
1838             raise unittest.SkipTest("Symlinks are not supported by Python on this platform.")
1839
1840         self.basedir = os.path.dirname(self.mktemp())
1841         self.set_up_grid()
1842
1843         source = os.path.join(self.basedir, "home")
1844         self.writeto("foo.txt", "foo")
1845         os.symlink(os.path.join(source, "foo.txt"), os.path.join(source, "foo2.txt"))
1846
1847         d = self.do_cli("create-alias", "tahoe")
1848         d.addCallback(lambda res: self.do_cli("backup", "--verbose", source, "tahoe:test"))
1849
1850         def _check((rc, out, err)):
1851             self.failUnlessReallyEqual(rc, 2)
1852             foo2 = os.path.join(source, "foo2.txt")
1853             self.failUnlessReallyEqual(err, "WARNING: cannot backup symlink '%s'\n" % foo2)
1854
1855             fu, fr, fs, dc, dr, ds = self.count_output(out)
1856             # foo.txt
1857             self.failUnlessReallyEqual(fu, 1)
1858             self.failUnlessReallyEqual(fr, 0)
1859             # foo2.txt
1860             self.failUnlessReallyEqual(fs, 1)
1861             # home
1862             self.failUnlessReallyEqual(dc, 1)
1863             self.failUnlessReallyEqual(dr, 0)
1864             self.failUnlessReallyEqual(ds, 0)
1865
1866         d.addCallback(_check)
1867         return d
1868
1869     def test_ignore_unreadable_file(self):
1870         self.basedir = os.path.dirname(self.mktemp())
1871         self.set_up_grid()
1872
1873         source = os.path.join(self.basedir, "home")
1874         self.writeto("foo.txt", "foo")
1875         os.chmod(os.path.join(source, "foo.txt"), 0000)
1876
1877         d = self.do_cli("create-alias", "tahoe")
1878         d.addCallback(lambda res: self.do_cli("backup", source, "tahoe:test"))
1879
1880         def _check((rc, out, err)):
1881             self.failUnlessReallyEqual(rc, 2)
1882             self.failUnlessReallyEqual(err, "WARNING: permission denied on file %s\n" % os.path.join(source, "foo.txt"))
1883
1884             fu, fr, fs, dc, dr, ds = self.count_output(out)
1885             self.failUnlessReallyEqual(fu, 0)
1886             self.failUnlessReallyEqual(fr, 0)
1887             # foo.txt
1888             self.failUnlessReallyEqual(fs, 1)
1889             # home
1890             self.failUnlessReallyEqual(dc, 1)
1891             self.failUnlessReallyEqual(dr, 0)
1892             self.failUnlessReallyEqual(ds, 0)
1893         d.addCallback(_check)
1894
1895         # This is necessary for the temp files to be correctly removed
1896         def _cleanup(self):
1897             os.chmod(os.path.join(source, "foo.txt"), 0644)
1898         d.addCallback(_cleanup)
1899         d.addErrback(_cleanup)
1900
1901         return d
1902
1903     def test_ignore_unreadable_directory(self):
1904         self.basedir = os.path.dirname(self.mktemp())
1905         self.set_up_grid()
1906
1907         source = os.path.join(self.basedir, "home")
1908         os.mkdir(source)
1909         os.mkdir(os.path.join(source, "test"))
1910         os.chmod(os.path.join(source, "test"), 0000)
1911
1912         d = self.do_cli("create-alias", "tahoe")
1913         d.addCallback(lambda res: self.do_cli("backup", source, "tahoe:test"))
1914
1915         def _check((rc, out, err)):
1916             self.failUnlessReallyEqual(rc, 2)
1917             self.failUnlessReallyEqual(err, "WARNING: permission denied on directory %s\n" % os.path.join(source, "test"))
1918
1919             fu, fr, fs, dc, dr, ds = self.count_output(out)
1920             self.failUnlessReallyEqual(fu, 0)
1921             self.failUnlessReallyEqual(fr, 0)
1922             self.failUnlessReallyEqual(fs, 0)
1923             # home, test
1924             self.failUnlessReallyEqual(dc, 2)
1925             self.failUnlessReallyEqual(dr, 0)
1926             # test
1927             self.failUnlessReallyEqual(ds, 1)
1928         d.addCallback(_check)
1929
1930         # This is necessary for the temp files to be correctly removed
1931         def _cleanup(self):
1932             os.chmod(os.path.join(source, "test"), 0655)
1933         d.addCallback(_cleanup)
1934         d.addErrback(_cleanup)
1935         return d
1936
1937     def test_backup_without_alias(self):
1938         # 'tahoe backup' should output a sensible error message when invoked
1939         # without an alias instead of a stack trace.
1940         self.basedir = os.path.dirname(self.mktemp())
1941         self.set_up_grid()
1942         source = os.path.join(self.basedir, "file1")
1943         d = self.do_cli('backup', source, source)
1944         def _check((rc, out, err)):
1945             self.failUnlessReallyEqual(rc, 1)
1946             self.failUnlessIn("error:", err)
1947             self.failUnlessReallyEqual(out, "")
1948         d.addCallback(_check)
1949         return d
1950
1951     def test_backup_with_nonexistent_alias(self):
1952         # 'tahoe backup' should output a sensible error message when invoked
1953         # with a nonexistent alias.
1954         self.basedir = os.path.dirname(self.mktemp())
1955         self.set_up_grid()
1956         source = os.path.join(self.basedir, "file1")
1957         d = self.do_cli("backup", source, "nonexistent:" + source)
1958         def _check((rc, out, err)):
1959             self.failUnlessReallyEqual(rc, 1)
1960             self.failUnlessIn("error:", err)
1961             self.failUnlessIn("nonexistent", err)
1962             self.failUnlessReallyEqual(out, "")
1963         d.addCallback(_check)
1964         return d
1965
1966
1967 class Check(GridTestMixin, CLITestMixin, unittest.TestCase):
1968
1969     def test_check(self):
1970         self.basedir = "cli/Check/check"
1971         self.set_up_grid()
1972         c0 = self.g.clients[0]
1973         DATA = "data" * 100
1974         d = c0.create_mutable_file(DATA)
1975         def _stash_uri(n):
1976             self.uri = n.get_uri()
1977         d.addCallback(_stash_uri)
1978
1979         d.addCallback(lambda ign: self.do_cli("check", self.uri))
1980         def _check1((rc, out, err)):
1981             self.failUnlessReallyEqual(err, "")
1982             self.failUnlessReallyEqual(rc, 0)
1983             lines = out.splitlines()
1984             self.failUnless("Summary: Healthy" in lines, out)
1985             self.failUnless(" good-shares: 10 (encoding is 3-of-10)" in lines, out)
1986         d.addCallback(_check1)
1987
1988         d.addCallback(lambda ign: self.do_cli("check", "--raw", self.uri))
1989         def _check2((rc, out, err)):
1990             self.failUnlessReallyEqual(err, "")
1991             self.failUnlessReallyEqual(rc, 0)
1992             data = simplejson.loads(out)
1993             self.failUnlessReallyEqual(to_str(data["summary"]), "Healthy")
1994         d.addCallback(_check2)
1995
1996         def _clobber_shares(ignored):
1997             # delete one, corrupt a second
1998             shares = self.find_uri_shares(self.uri)
1999             self.failUnlessReallyEqual(len(shares), 10)
2000             os.unlink(shares[0][2])
2001             cso = debug.CorruptShareOptions()
2002             cso.stdout = StringIO()
2003             cso.parseOptions([shares[1][2]])
2004             storage_index = uri.from_string(self.uri).get_storage_index()
2005             self._corrupt_share_line = "  server %s, SI %s, shnum %d" % \
2006                                        (base32.b2a(shares[1][1]),
2007                                         base32.b2a(storage_index),
2008                                         shares[1][0])
2009             debug.corrupt_share(cso)
2010         d.addCallback(_clobber_shares)
2011
2012         d.addCallback(lambda ign: self.do_cli("check", "--verify", self.uri))
2013         def _check3((rc, out, err)):
2014             self.failUnlessReallyEqual(err, "")
2015             self.failUnlessReallyEqual(rc, 0)
2016             lines = out.splitlines()
2017             summary = [l for l in lines if l.startswith("Summary")][0]
2018             self.failUnless("Summary: Unhealthy: 8 shares (enc 3-of-10)"
2019                             in summary, summary)
2020             self.failUnless(" good-shares: 8 (encoding is 3-of-10)" in lines, out)
2021             self.failUnless(" corrupt shares:" in lines, out)
2022             self.failUnless(self._corrupt_share_line in lines, out)
2023         d.addCallback(_check3)
2024
2025         d.addCallback(lambda ign:
2026                       self.do_cli("check", "--verify", "--repair", self.uri))
2027         def _check4((rc, out, err)):
2028             self.failUnlessReallyEqual(err, "")
2029             self.failUnlessReallyEqual(rc, 0)
2030             lines = out.splitlines()
2031             self.failUnless("Summary: not healthy" in lines, out)
2032             self.failUnless(" good-shares: 8 (encoding is 3-of-10)" in lines, out)
2033             self.failUnless(" corrupt shares:" in lines, out)
2034             self.failUnless(self._corrupt_share_line in lines, out)
2035             self.failUnless(" repair successful" in lines, out)
2036         d.addCallback(_check4)
2037
2038         d.addCallback(lambda ign:
2039                       self.do_cli("check", "--verify", "--repair", self.uri))
2040         def _check5((rc, out, err)):
2041             self.failUnlessReallyEqual(err, "")
2042             self.failUnlessReallyEqual(rc, 0)
2043             lines = out.splitlines()
2044             self.failUnless("Summary: healthy" in lines, out)
2045             self.failUnless(" good-shares: 10 (encoding is 3-of-10)" in lines, out)
2046             self.failIf(" corrupt shares:" in lines, out)
2047         d.addCallback(_check5)
2048
2049         return d
2050
2051     def test_deep_check(self):
2052         self.basedir = "cli/Check/deep_check"
2053         self.set_up_grid()
2054         c0 = self.g.clients[0]
2055         self.uris = {}
2056         self.fileurls = {}
2057         DATA = "data" * 100
2058         quoted_good = quote_output(u"g\u00F6\u00F6d")
2059
2060         d = c0.create_dirnode()
2061         def _stash_root_and_create_file(n):
2062             self.rootnode = n
2063             self.rooturi = n.get_uri()
2064             return n.add_file(u"g\u00F6\u00F6d", upload.Data(DATA, convergence=""))
2065         d.addCallback(_stash_root_and_create_file)
2066         def _stash_uri(fn, which):
2067             self.uris[which] = fn.get_uri()
2068             return fn
2069         d.addCallback(_stash_uri, u"g\u00F6\u00F6d")
2070         d.addCallback(lambda ign:
2071                       self.rootnode.add_file(u"small",
2072                                            upload.Data("literal",
2073                                                         convergence="")))
2074         d.addCallback(_stash_uri, "small")
2075         d.addCallback(lambda ign: c0.create_mutable_file(DATA+"1"))
2076         d.addCallback(lambda fn: self.rootnode.set_node(u"mutable", fn))
2077         d.addCallback(_stash_uri, "mutable")
2078
2079         d.addCallback(lambda ign: self.do_cli("deep-check", self.rooturi))
2080         def _check1((rc, out, err)):
2081             self.failUnlessReallyEqual(err, "")
2082             self.failUnlessReallyEqual(rc, 0)
2083             lines = out.splitlines()
2084             self.failUnless("done: 4 objects checked, 4 healthy, 0 unhealthy"
2085                             in lines, out)
2086         d.addCallback(_check1)
2087
2088         # root
2089         # root/g\u00F6\u00F6d
2090         # root/small
2091         # root/mutable
2092
2093         d.addCallback(lambda ign: self.do_cli("deep-check", "--verbose",
2094                                               self.rooturi))
2095         def _check2((rc, out, err)):
2096             self.failUnlessReallyEqual(err, "")
2097             self.failUnlessReallyEqual(rc, 0)
2098             lines = out.splitlines()
2099             self.failUnless("'<root>': Healthy" in lines, out)
2100             self.failUnless("'small': Healthy (LIT)" in lines, out)
2101             self.failUnless((quoted_good + ": Healthy") in lines, out)
2102             self.failUnless("'mutable': Healthy" in lines, out)
2103             self.failUnless("done: 4 objects checked, 4 healthy, 0 unhealthy"
2104                             in lines, out)
2105         d.addCallback(_check2)
2106
2107         d.addCallback(lambda ign: self.do_cli("stats", self.rooturi))
2108         def _check_stats((rc, out, err)):
2109             self.failUnlessReallyEqual(err, "")
2110             self.failUnlessReallyEqual(rc, 0)
2111             lines = out.splitlines()
2112             self.failUnlessIn(" count-immutable-files: 1", lines)
2113             self.failUnlessIn("   count-mutable-files: 1", lines)
2114             self.failUnlessIn("   count-literal-files: 1", lines)
2115             self.failUnlessIn("     count-directories: 1", lines)
2116             self.failUnlessIn("  size-immutable-files: 400", lines)
2117             self.failUnlessIn("Size Histogram:", lines)
2118             self.failUnlessIn("   4-10   : 1    (10 B, 10 B)", lines)
2119             self.failUnlessIn(" 317-1000 : 1    (1000 B, 1000 B)", lines)
2120         d.addCallback(_check_stats)
2121
2122         def _clobber_shares(ignored):
2123             shares = self.find_uri_shares(self.uris[u"g\u00F6\u00F6d"])
2124             self.failUnlessReallyEqual(len(shares), 10)
2125             os.unlink(shares[0][2])
2126
2127             shares = self.find_uri_shares(self.uris["mutable"])
2128             cso = debug.CorruptShareOptions()
2129             cso.stdout = StringIO()
2130             cso.parseOptions([shares[1][2]])
2131             storage_index = uri.from_string(self.uris["mutable"]).get_storage_index()
2132             self._corrupt_share_line = " corrupt: server %s, SI %s, shnum %d" % \
2133                                        (base32.b2a(shares[1][1]),
2134                                         base32.b2a(storage_index),
2135                                         shares[1][0])
2136             debug.corrupt_share(cso)
2137         d.addCallback(_clobber_shares)
2138
2139         # root
2140         # root/g\u00F6\u00F6d  [9 shares]
2141         # root/small
2142         # root/mutable [1 corrupt share]
2143
2144         d.addCallback(lambda ign:
2145                       self.do_cli("deep-check", "--verbose", self.rooturi))
2146         def _check3((rc, out, err)):
2147             self.failUnlessReallyEqual(err, "")
2148             self.failUnlessReallyEqual(rc, 0)
2149             lines = out.splitlines()
2150             self.failUnless("'<root>': Healthy" in lines, out)
2151             self.failUnless("'small': Healthy (LIT)" in lines, out)
2152             self.failUnless("'mutable': Healthy" in lines, out) # needs verifier
2153             self.failUnless((quoted_good + ": Not Healthy: 9 shares (enc 3-of-10)") in lines, out)
2154             self.failIf(self._corrupt_share_line in lines, out)
2155             self.failUnless("done: 4 objects checked, 3 healthy, 1 unhealthy"
2156                             in lines, out)
2157         d.addCallback(_check3)
2158
2159         d.addCallback(lambda ign:
2160                       self.do_cli("deep-check", "--verbose", "--verify",
2161                                   self.rooturi))
2162         def _check4((rc, out, err)):
2163             self.failUnlessReallyEqual(err, "")
2164             self.failUnlessReallyEqual(rc, 0)
2165             lines = out.splitlines()
2166             self.failUnless("'<root>': Healthy" in lines, out)
2167             self.failUnless("'small': Healthy (LIT)" in lines, out)
2168             mutable = [l for l in lines if l.startswith("'mutable'")][0]
2169             self.failUnless(mutable.startswith("'mutable': Unhealthy: 9 shares (enc 3-of-10)"),
2170                             mutable)
2171             self.failUnless(self._corrupt_share_line in lines, out)
2172             self.failUnless((quoted_good + ": Not Healthy: 9 shares (enc 3-of-10)") in lines, out)
2173             self.failUnless("done: 4 objects checked, 2 healthy, 2 unhealthy"
2174                             in lines, out)
2175         d.addCallback(_check4)
2176
2177         d.addCallback(lambda ign:
2178                       self.do_cli("deep-check", "--raw",
2179                                   self.rooturi))
2180         def _check5((rc, out, err)):
2181             self.failUnlessReallyEqual(err, "")
2182             self.failUnlessReallyEqual(rc, 0)
2183             lines = out.splitlines()
2184             units = [simplejson.loads(line) for line in lines]
2185             # root, small, g\u00F6\u00F6d, mutable,  stats
2186             self.failUnlessReallyEqual(len(units), 4+1)
2187         d.addCallback(_check5)
2188
2189         d.addCallback(lambda ign:
2190                       self.do_cli("deep-check",
2191                                   "--verbose", "--verify", "--repair",
2192                                   self.rooturi))
2193         def _check6((rc, out, err)):
2194             self.failUnlessReallyEqual(err, "")
2195             self.failUnlessReallyEqual(rc, 0)
2196             lines = out.splitlines()
2197             self.failUnless("'<root>': healthy" in lines, out)
2198             self.failUnless("'small': healthy" in lines, out)
2199             self.failUnless("'mutable': not healthy" in lines, out)
2200             self.failUnless(self._corrupt_share_line in lines, out)
2201             self.failUnless((quoted_good + ": not healthy") in lines, out)
2202             self.failUnless("done: 4 objects checked" in lines, out)
2203             self.failUnless(" pre-repair: 2 healthy, 2 unhealthy" in lines, out)
2204             self.failUnless(" 2 repairs attempted, 2 successful, 0 failed"
2205                             in lines, out)
2206             self.failUnless(" post-repair: 4 healthy, 0 unhealthy" in lines,out)
2207         d.addCallback(_check6)
2208
2209         # now add a subdir, and a file below that, then make the subdir
2210         # unrecoverable
2211
2212         d.addCallback(lambda ign: self.rootnode.create_subdirectory(u"subdir"))
2213         d.addCallback(_stash_uri, "subdir")
2214         d.addCallback(lambda fn:
2215                       fn.add_file(u"subfile", upload.Data(DATA+"2", "")))
2216         d.addCallback(lambda ign:
2217                       self.delete_shares_numbered(self.uris["subdir"],
2218                                                   range(10)))
2219
2220         # root
2221         # rootg\u00F6\u00F6d/
2222         # root/small
2223         # root/mutable
2224         # root/subdir [unrecoverable: 0 shares]
2225         # root/subfile
2226
2227         d.addCallback(lambda ign: self.do_cli("manifest", self.rooturi))
2228         def _manifest_failed((rc, out, err)):
2229             self.failIfEqual(rc, 0)
2230             self.failUnlessIn("ERROR: UnrecoverableFileError", err)
2231             # the fatal directory should still show up, as the last line
2232             self.failUnlessIn(" subdir\n", out)
2233         d.addCallback(_manifest_failed)
2234
2235         d.addCallback(lambda ign: self.do_cli("deep-check", self.rooturi))
2236         def _deep_check_failed((rc, out, err)):
2237             self.failIfEqual(rc, 0)
2238             self.failUnlessIn("ERROR: UnrecoverableFileError", err)
2239             # we want to make sure that the error indication is the last
2240             # thing that gets emitted
2241             self.failIf("done:" in out, out)
2242         d.addCallback(_deep_check_failed)
2243
2244         # this test is disabled until the deep-repair response to an
2245         # unrepairable directory is fixed. The failure-to-repair should not
2246         # throw an exception, but the failure-to-traverse that follows
2247         # should throw UnrecoverableFileError.
2248
2249         #d.addCallback(lambda ign:
2250         #              self.do_cli("deep-check", "--repair", self.rooturi))
2251         #def _deep_check_repair_failed((rc, out, err)):
2252         #    self.failIfEqual(rc, 0)
2253         #    print err
2254         #    self.failUnlessIn("ERROR: UnrecoverableFileError", err)
2255         #    self.failIf("done:" in out, out)
2256         #d.addCallback(_deep_check_repair_failed)
2257
2258         return d
2259
2260     def test_check_without_alias(self):
2261         # 'tahoe check' should output a sensible error message if it needs to
2262         # find the default alias and can't
2263         self.basedir = "cli/Check/check_without_alias"
2264         self.set_up_grid()
2265         d = self.do_cli("check")
2266         def _check((rc, out, err)):
2267             self.failUnlessReallyEqual(rc, 1)
2268             self.failUnlessIn("error:", err)
2269             self.failUnlessReallyEqual(out, "")
2270         d.addCallback(_check)
2271         d.addCallback(lambda ign: self.do_cli("deep-check"))
2272         d.addCallback(_check)
2273         return d
2274
2275     def test_check_with_nonexistent_alias(self):
2276         # 'tahoe check' should output a sensible error message if it needs to
2277         # find an alias and can't.
2278         self.basedir = "cli/Check/check_with_nonexistent_alias"
2279         self.set_up_grid()
2280         d = self.do_cli("check", "nonexistent:")
2281         def _check((rc, out, err)):
2282             self.failUnlessReallyEqual(rc, 1)
2283             self.failUnlessIn("error:", err)
2284             self.failUnlessIn("nonexistent", err)
2285             self.failUnlessReallyEqual(out, "")
2286         d.addCallback(_check)
2287         return d
2288
2289
2290 class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
2291     def test_get(self):
2292         self.basedir = "cli/Errors/get"
2293         self.set_up_grid()
2294         c0 = self.g.clients[0]
2295         self.fileurls = {}
2296         DATA = "data" * 100
2297         d = c0.upload(upload.Data(DATA, convergence=""))
2298         def _stash_bad(ur):
2299             self.uri_1share = ur.uri
2300             self.delete_shares_numbered(ur.uri, range(1,10))
2301         d.addCallback(_stash_bad)
2302
2303         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share))
2304         def _check1((rc, out, err)):
2305             self.failIfEqual(rc, 0)
2306             self.failUnless("410 Gone" in err, err)
2307             self.failUnlessIn("NotEnoughSharesError: ", err)
2308             self.failUnlessIn("Failed to get enough shareholders: have 1, need 3", err)
2309         d.addCallback(_check1)
2310
2311         targetf = os.path.join(self.basedir, "output")
2312         d.addCallback(lambda ign: self.do_cli("get", self.uri_1share, targetf))
2313         def _check2((rc, out, err)):
2314             self.failIfEqual(rc, 0)
2315             self.failUnless("410 Gone" in err, err)
2316             self.failUnlessIn("NotEnoughSharesError: ", err)
2317             self.failUnlessIn("Failed to get enough shareholders: have 1, need 3", err)
2318             self.failIf(os.path.exists(targetf))
2319         d.addCallback(_check2)
2320
2321         return d
2322
2323
2324 class Get(GridTestMixin, CLITestMixin, unittest.TestCase):
2325     def test_get_without_alias(self):
2326         # 'tahoe get' should output a useful error message when invoked
2327         # without an explicit alias and when the default 'tahoe' alias
2328         # hasn't been created yet.
2329         self.basedir = "cli/Get/get_without_alias"
2330         self.set_up_grid()
2331         d = self.do_cli('get', 'file')
2332         def _check((rc, out, err)):
2333             self.failUnlessReallyEqual(rc, 1)
2334             self.failUnlessIn("error:", err)
2335             self.failUnlessReallyEqual(out, "")
2336         d.addCallback(_check)
2337         return d
2338
2339     def test_get_with_nonexistent_alias(self):
2340         # 'tahoe get' should output a useful error message when invoked with
2341         # an explicit alias that doesn't exist.
2342         self.basedir = "cli/Get/get_with_nonexistent_alias"
2343         self.set_up_grid()
2344         d = self.do_cli("get", "nonexistent:file")
2345         def _check((rc, out, err)):
2346             self.failUnlessReallyEqual(rc, 1)
2347             self.failUnlessIn("error:", err)
2348             self.failUnlessIn("nonexistent", err)
2349             self.failUnlessReallyEqual(out, "")
2350         d.addCallback(_check)
2351         return d
2352
2353
2354 class Manifest(GridTestMixin, CLITestMixin, unittest.TestCase):
2355     def test_manifest_without_alias(self):
2356         # 'tahoe manifest' should output a useful error message when invoked
2357         # without an explicit alias when the default 'tahoe' alias is
2358         # missing.
2359         self.basedir = "cli/Manifest/manifest_without_alias"
2360         self.set_up_grid()
2361         d = self.do_cli("manifest")
2362         def _check((rc, out, err)):
2363             self.failUnlessReallyEqual(rc, 1)
2364             self.failUnlessIn("error:", err)
2365             self.failUnlessReallyEqual(out, "")
2366         d.addCallback(_check)
2367         return d
2368
2369     def test_manifest_with_nonexistent_alias(self):
2370         # 'tahoe manifest' should output a useful error message when invoked
2371         # with an explicit alias that doesn't exist.
2372         self.basedir = "cli/Manifest/manifest_with_nonexistent_alias"
2373         self.set_up_grid()
2374         d = self.do_cli("manifest", "nonexistent:")
2375         def _check((rc, out, err)):
2376             self.failUnlessReallyEqual(rc, 1)
2377             self.failUnlessIn("error:", err)
2378             self.failUnlessIn("nonexistent", err)
2379             self.failUnlessReallyEqual(out, "")
2380         d.addCallback(_check)
2381         return d
2382
2383
2384 class Mkdir(GridTestMixin, CLITestMixin, unittest.TestCase):
2385     def test_mkdir(self):
2386         self.basedir = os.path.dirname(self.mktemp())
2387         self.set_up_grid()
2388
2389         d = self.do_cli("create-alias", "tahoe")
2390         d.addCallback(lambda res: self.do_cli("mkdir", "test"))
2391         def _check((rc, out, err)):
2392             self.failUnlessReallyEqual(rc, 0)
2393             self.failUnlessReallyEqual(err, "")
2394             self.failUnlessIn("URI:", out)
2395         d.addCallback(_check)
2396
2397         return d
2398
2399     def test_mkdir_unicode(self):
2400         self.basedir = os.path.dirname(self.mktemp())
2401         self.set_up_grid()
2402
2403         try:
2404             motorhead_arg = u"tahoe:Mot\u00F6rhead".encode(get_argv_encoding())
2405         except UnicodeEncodeError:
2406             raise unittest.SkipTest("A non-ASCII command argument could not be encoded on this platform.")
2407
2408         d = self.do_cli("create-alias", "tahoe")
2409         d.addCallback(lambda res: self.do_cli("mkdir", motorhead_arg))
2410         def _check((rc, out, err)):
2411             self.failUnlessReallyEqual(rc, 0)
2412             self.failUnlessReallyEqual(err, "")
2413             self.failUnlessIn("URI:", out)
2414         d.addCallback(_check)
2415
2416         return d
2417
2418     def test_mkdir_with_nonexistent_alias(self):
2419         # when invoked with an alias that doesn't exist, 'tahoe mkdir' should
2420         # output a sensible error message rather than a stack trace.
2421         self.basedir = "cli/Mkdir/mkdir_with_nonexistent_alias"
2422         self.set_up_grid()
2423         d = self.do_cli("mkdir", "havasu:")
2424         def _check((rc, out, err)):
2425             self.failUnlessReallyEqual(rc, 1)
2426             self.failUnlessIn("error:", err)
2427             self.failUnlessReallyEqual(out, "")
2428         d.addCallback(_check)
2429         return d
2430
2431
2432 class Rm(GridTestMixin, CLITestMixin, unittest.TestCase):
2433     def test_rm_without_alias(self):
2434         # 'tahoe rm' should behave sensibly when invoked without an explicit
2435         # alias before the default 'tahoe' alias has been created.
2436         self.basedir = "cli/Rm/rm_without_alias"
2437         self.set_up_grid()
2438         d = self.do_cli("rm", "afile")
2439         def _check((rc, out, err)):
2440             self.failUnlessReallyEqual(rc, 1)
2441             self.failUnlessIn("error:", err)
2442             self.failUnlessReallyEqual(out, "")
2443         d.addCallback(_check)
2444
2445         d.addCallback(lambda ign: self.do_cli("unlink", "afile"))
2446         d.addCallback(_check)
2447         return d
2448
2449     def test_rm_with_nonexistent_alias(self):
2450         # 'tahoe rm' should behave sensibly when invoked with an explicit
2451         # alias that doesn't exist.
2452         self.basedir = "cli/Rm/rm_with_nonexistent_alias"
2453         self.set_up_grid()
2454         d = self.do_cli("rm", "nonexistent:afile")
2455         def _check((rc, out, err)):
2456             self.failUnlessReallyEqual(rc, 1)
2457             self.failUnlessIn("error:", err)
2458             self.failUnlessIn("nonexistent", err)
2459             self.failUnlessReallyEqual(out, "")
2460         d.addCallback(_check)
2461
2462         d.addCallback(lambda ign: self.do_cli("unlink", "nonexistent:afile"))
2463         d.addCallback(_check)
2464         return d
2465
2466
2467 class Stats(GridTestMixin, CLITestMixin, unittest.TestCase):
2468     def test_empty_directory(self):
2469         self.basedir = "cli/Stats/empty_directory"
2470         self.set_up_grid()
2471         c0 = self.g.clients[0]
2472         self.fileurls = {}
2473         d = c0.create_dirnode()
2474         def _stash_root(n):
2475             self.rootnode = n
2476             self.rooturi = n.get_uri()
2477         d.addCallback(_stash_root)
2478
2479         # make sure we can get stats on an empty directory too
2480         d.addCallback(lambda ign: self.do_cli("stats", self.rooturi))
2481         def _check_stats((rc, out, err)):
2482             self.failUnlessReallyEqual(err, "")
2483             self.failUnlessReallyEqual(rc, 0)
2484             lines = out.splitlines()
2485             self.failUnlessIn(" count-immutable-files: 0", lines)
2486             self.failUnlessIn("   count-mutable-files: 0", lines)
2487             self.failUnlessIn("   count-literal-files: 0", lines)
2488             self.failUnlessIn("     count-directories: 1", lines)
2489             self.failUnlessIn("  size-immutable-files: 0", lines)
2490             self.failIfIn("Size Histogram:", lines)
2491         d.addCallback(_check_stats)
2492
2493         return d
2494
2495     def test_stats_without_alias(self):
2496         # when invoked with no explicit alias and before the default 'tahoe'
2497         # alias is created, 'tahoe stats' should output an informative error
2498         # message, not a stack trace.
2499         self.basedir = "cli/Stats/stats_without_alias"
2500         self.set_up_grid()
2501         d = self.do_cli("stats")
2502         def _check((rc, out, err)):
2503             self.failUnlessReallyEqual(rc, 1)
2504             self.failUnlessIn("error:", err)
2505             self.failUnlessReallyEqual(out, "")
2506         d.addCallback(_check)
2507         return d
2508
2509     def test_stats_with_nonexistent_alias(self):
2510         # when invoked with an explicit alias that doesn't exist,
2511         # 'tahoe stats' should output a useful error message.
2512         self.basedir = "cli/Stats/stats_with_nonexistent_alias"
2513         self.set_up_grid()
2514         d = self.do_cli("stats", "havasu:")
2515         def _check((rc, out, err)):
2516             self.failUnlessReallyEqual(rc, 1)
2517             self.failUnlessIn("error:", err)
2518             self.failUnlessReallyEqual(out, "")
2519         d.addCallback(_check)
2520         return d
2521
2522
2523 class Webopen(GridTestMixin, CLITestMixin, unittest.TestCase):
2524     def test_webopen_with_nonexistent_alias(self):
2525         # when invoked with an alias that doesn't exist, 'tahoe webopen'
2526         # should output an informative error message instead of a stack
2527         # trace.
2528         self.basedir = "cli/Webopen/webopen_with_nonexistent_alias"
2529         self.set_up_grid()
2530         d = self.do_cli("webopen", "fake:")
2531         def _check((rc, out, err)):
2532             self.failUnlessReallyEqual(rc, 1)
2533             self.failUnlessIn("error:", err)
2534             self.failUnlessReallyEqual(out, "")
2535         d.addCallback(_check)
2536         return d