]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_cli.py
test_cli/test_web: fix spurious test failure on solaris (maybe python2.4?) due to...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_cli.py
1 # coding=utf-8
2
3 import os.path
4 from twisted.trial import unittest
5 from cStringIO import StringIO
6 import urllib
7 import re
8 import simplejson
9
10 from allmydata.util import fileutil, hashutil, base32
11 from allmydata import uri
12 from allmydata.immutable import upload
13
14 # Test that the scripts can be imported -- although the actual tests of their functionality are
15 # done by invoking them in a subprocess.
16 from allmydata.scripts import tahoe_ls, tahoe_get, tahoe_put, tahoe_rm, tahoe_cp
17 _hush_pyflakes = [tahoe_ls, tahoe_get, tahoe_put, tahoe_rm, tahoe_cp]
18
19 from allmydata.scripts import common
20 from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases, get_alias, \
21      DefaultAliasMarker
22
23 from allmydata.scripts import cli, debug, runner, backupdb
24 from allmydata.test.common_util import StallMixin
25 from allmydata.test.no_network import GridTestMixin
26 from twisted.internet import threads # CLI tests use deferToThread
27 from twisted.python import usage
28
29 class CLI(unittest.TestCase):
30     # this test case only looks at argument-processing and simple stuff.
31     def test_options(self):
32         fileutil.rm_dir("cli/test_options")
33         fileutil.make_dirs("cli/test_options")
34         fileutil.make_dirs("cli/test_options/private")
35         open("cli/test_options/node.url","w").write("http://localhost:8080/\n")
36         filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
37                                                fingerprint="\x00"*32)
38         private_uri = uri.NewDirectoryURI(filenode_uri).to_string()
39         open("cli/test_options/private/root_dir.cap", "w").write(private_uri + "\n")
40         o = cli.ListOptions()
41         o.parseOptions(["--node-directory", "cli/test_options"])
42         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
43         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
44         self.failUnlessEqual(o.where, "")
45
46         o = cli.ListOptions()
47         o.parseOptions(["--node-directory", "cli/test_options",
48                         "--node-url", "http://example.org:8111/"])
49         self.failUnlessEqual(o['node-url'], "http://example.org:8111/")
50         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
51         self.failUnlessEqual(o.where, "")
52
53         o = cli.ListOptions()
54         o.parseOptions(["--node-directory", "cli/test_options",
55                         "--dir-cap", "root"])
56         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
57         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], "root")
58         self.failUnlessEqual(o.where, "")
59
60         o = cli.ListOptions()
61         other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
62                                                      fingerprint="\x11"*32)
63         other_uri = uri.NewDirectoryURI(other_filenode_uri).to_string()
64         o.parseOptions(["--node-directory", "cli/test_options",
65                         "--dir-cap", other_uri])
66         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
67         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
68         self.failUnlessEqual(o.where, "")
69
70         o = cli.ListOptions()
71         o.parseOptions(["--node-directory", "cli/test_options",
72                         "--dir-cap", other_uri, "subdir"])
73         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
74         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
75         self.failUnlessEqual(o.where, "subdir")
76
77         o = cli.ListOptions()
78         self.failUnlessRaises(usage.UsageError,
79                               o.parseOptions,
80                               ["--node-directory", "cli/test_options",
81                                "--node-url", "NOT-A-URL"])
82
83         o = cli.ListOptions()
84         o.parseOptions(["--node-directory", "cli/test_options",
85                         "--node-url", "http://localhost:8080"])
86         self.failUnlessEqual(o["node-url"], "http://localhost:8080/")
87
88     def _dump_cap(self, *args):
89         config = debug.DumpCapOptions()
90         config.stdout,config.stderr = StringIO(), StringIO()
91         config.parseOptions(args)
92         debug.dump_cap(config)
93         self.failIf(config.stderr.getvalue())
94         output = config.stdout.getvalue()
95         return output
96
97     def test_dump_cap_chk(self):
98         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
99         storage_index = hashutil.storage_index_hash(key)
100         uri_extension_hash = hashutil.uri_extension_hash("stuff")
101         needed_shares = 25
102         total_shares = 100
103         size = 1234
104         u = uri.CHKFileURI(key=key,
105                            uri_extension_hash=uri_extension_hash,
106                            needed_shares=needed_shares,
107                            total_shares=total_shares,
108                            size=size)
109         output = self._dump_cap(u.to_string())
110         self.failUnless("CHK File:" in output, output)
111         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
112         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
113         self.failUnless("size: 1234" in output, output)
114         self.failUnless("k/N: 25/100" in output, output)
115         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
116
117         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
118                                 u.to_string())
119         self.failUnless("client renewal secret: znxmki5zdibb5qlt46xbdvk2t55j7hibejq3i5ijyurkr6m6jkhq" in output, output)
120
121         output = self._dump_cap(u.get_verify_cap().to_string())
122         self.failIf("key: " in output, output)
123         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
124         self.failUnless("size: 1234" in output, output)
125         self.failUnless("k/N: 25/100" in output, output)
126         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
127
128         prefixed_u = "http://127.0.0.1/uri/%s" % urllib.quote(u.to_string())
129         output = self._dump_cap(prefixed_u)
130         self.failUnless("CHK File:" in output, output)
131         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
132         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
133         self.failUnless("size: 1234" in output, output)
134         self.failUnless("k/N: 25/100" in output, output)
135         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
136
137     def test_dump_cap_lit(self):
138         u = uri.LiteralFileURI("this is some data")
139         output = self._dump_cap(u.to_string())
140         self.failUnless("Literal File URI:" in output, output)
141         self.failUnless("data: this is some data" in output, output)
142
143     def test_dump_cap_ssk(self):
144         writekey = "\x01" * 16
145         fingerprint = "\xfe" * 32
146         u = uri.WriteableSSKFileURI(writekey, fingerprint)
147
148         output = self._dump_cap(u.to_string())
149         self.failUnless("SSK Writeable URI:" in output, output)
150         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output, output)
151         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
152         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
153         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
154
155         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
156                                 u.to_string())
157         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
158
159         fileutil.make_dirs("cli/test_dump_cap/private")
160         f = open("cli/test_dump_cap/private/secret", "w")
161         f.write("5s33nk3qpvnj2fw3z4mnm2y6fa\n")
162         f.close()
163         output = self._dump_cap("--client-dir", "cli/test_dump_cap",
164                                 u.to_string())
165         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
166
167         output = self._dump_cap("--client-dir", "cli/test_dump_cap_BOGUS",
168                                 u.to_string())
169         self.failIf("file renewal secret:" in output, output)
170
171         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
172                                 u.to_string())
173         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
174         self.failIf("file renewal secret:" in output, output)
175
176         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
177                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
178                                 u.to_string())
179         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
180         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
181         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
182
183         u = u.get_readonly()
184         output = self._dump_cap(u.to_string())
185         self.failUnless("SSK Read-only URI:" in output, output)
186         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
187         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
188         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
189
190         u = u.get_verify_cap()
191         output = self._dump_cap(u.to_string())
192         self.failUnless("SSK Verifier URI:" in output, output)
193         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
194         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
195
196     def test_dump_cap_directory(self):
197         writekey = "\x01" * 16
198         fingerprint = "\xfe" * 32
199         u1 = uri.WriteableSSKFileURI(writekey, fingerprint)
200         u = uri.NewDirectoryURI(u1)
201
202         output = self._dump_cap(u.to_string())
203         self.failUnless("Directory Writeable URI:" in output, output)
204         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output,
205                         output)
206         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
207         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output,
208                         output)
209         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
210
211         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
212                                 u.to_string())
213         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
214
215         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
216                                 u.to_string())
217         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
218         self.failIf("file renewal secret:" in output, output)
219
220         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
221                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
222                                 u.to_string())
223         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
224         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
225         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
226
227         u = u.get_readonly()
228         output = self._dump_cap(u.to_string())
229         self.failUnless("Directory Read-only URI:" in output, output)
230         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
231         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
232         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
233
234         u = u.get_verify_cap()
235         output = self._dump_cap(u.to_string())
236         self.failUnless("Directory Verifier URI:" in output, output)
237         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
238         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
239
240     def _catalog_shares(self, *basedirs):
241         o = debug.CatalogSharesOptions()
242         o.stdout,o.stderr = StringIO(), StringIO()
243         args = list(basedirs)
244         o.parseOptions(args)
245         debug.catalog_shares(o)
246         out = o.stdout.getvalue()
247         err = o.stderr.getvalue()
248         return out, err
249
250     def test_catalog_shares_error(self):
251         nodedir1 = "cli/test_catalog_shares/node1"
252         sharedir = os.path.join(nodedir1, "storage", "shares", "mq", "mqfblse6m5a6dh45isu2cg7oji")
253         fileutil.make_dirs(sharedir)
254         f = open(os.path.join(sharedir, "8"), "wb")
255         open("cli/test_catalog_shares/node1/storage/shares/mq/not-a-dir", "wb").close()
256         # write a bogus share that looks a little bit like CHK
257         f.write("\x00\x00\x00\x01" + "\xff" * 200) # this triggers an assert
258         f.close()
259
260         nodedir2 = "cli/test_catalog_shares/node2"
261         fileutil.make_dirs(nodedir2)
262         open("cli/test_catalog_shares/node1/storage/shares/not-a-dir", "wb").close()
263
264         # now make sure that the 'catalog-shares' commands survives the error
265         out, err = self._catalog_shares(nodedir1, nodedir2)
266         self.failUnlessEqual(out, "", out)
267         self.failUnless("Error processing " in err,
268                         "didn't see 'error processing' in '%s'" % err)
269         #self.failUnless(nodedir1 in err,
270         #                "didn't see '%s' in '%s'" % (nodedir1, err))
271         # windows mangles the path, and os.path.join isn't enough to make
272         # up for it, so just look for individual strings
273         self.failUnless("node1" in err,
274                         "didn't see 'node1' in '%s'" % err)
275         self.failUnless("mqfblse6m5a6dh45isu2cg7oji" in err,
276                         "didn't see 'mqfblse6m5a6dh45isu2cg7oji' in '%s'" % err)
277
278     def test_alias(self):
279         aliases = {"tahoe": "TA",
280                    "work": "WA",
281                    "c": "CA"}
282         def ga1(path):
283             return get_alias(aliases, path, "tahoe")
284         uses_lettercolon = common.platform_uses_lettercolon_drivename()
285         self.failUnlessEqual(ga1("bare"), ("TA", "bare"))
286         self.failUnlessEqual(ga1("baredir/file"), ("TA", "baredir/file"))
287         self.failUnlessEqual(ga1("baredir/file:7"), ("TA", "baredir/file:7"))
288         self.failUnlessEqual(ga1("tahoe:"), ("TA", ""))
289         self.failUnlessEqual(ga1("tahoe:file"), ("TA", "file"))
290         self.failUnlessEqual(ga1("tahoe:dir/file"), ("TA", "dir/file"))
291         self.failUnlessEqual(ga1("work:"), ("WA", ""))
292         self.failUnlessEqual(ga1("work:file"), ("WA", "file"))
293         self.failUnlessEqual(ga1("work:dir/file"), ("WA", "dir/file"))
294         # default != None means we really expect a tahoe path, regardless of
295         # whether we're on windows or not. This is what 'tahoe get' uses.
296         self.failUnlessEqual(ga1("c:"), ("CA", ""))
297         self.failUnlessEqual(ga1("c:file"), ("CA", "file"))
298         self.failUnlessEqual(ga1("c:dir/file"), ("CA", "dir/file"))
299         self.failUnlessEqual(ga1("URI:stuff"), ("URI:stuff", ""))
300         self.failUnlessEqual(ga1("URI:stuff:./file"), ("URI:stuff", "file"))
301         self.failUnlessEqual(ga1("URI:stuff:./dir/file"),
302                              ("URI:stuff", "dir/file"))
303         self.failUnlessRaises(common.UnknownAliasError, ga1, "missing:")
304         self.failUnlessRaises(common.UnknownAliasError, ga1, "missing:dir")
305         self.failUnlessRaises(common.UnknownAliasError, ga1, "missing:dir/file")
306
307         def ga2(path):
308             return get_alias(aliases, path, None)
309         self.failUnlessEqual(ga2("bare"), (DefaultAliasMarker, "bare"))
310         self.failUnlessEqual(ga2("baredir/file"),
311                              (DefaultAliasMarker, "baredir/file"))
312         self.failUnlessEqual(ga2("baredir/file:7"),
313                              (DefaultAliasMarker, "baredir/file:7"))
314         self.failUnlessEqual(ga2("baredir/sub:1/file:7"),
315                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
316         self.failUnlessEqual(ga2("tahoe:"), ("TA", ""))
317         self.failUnlessEqual(ga2("tahoe:file"), ("TA", "file"))
318         self.failUnlessEqual(ga2("tahoe:dir/file"), ("TA", "dir/file"))
319         # on windows, we really want c:foo to indicate a local file.
320         # default==None is what 'tahoe cp' uses.
321         if uses_lettercolon:
322             self.failUnlessEqual(ga2("c:"), (DefaultAliasMarker, "c:"))
323             self.failUnlessEqual(ga2("c:file"), (DefaultAliasMarker, "c:file"))
324             self.failUnlessEqual(ga2("c:dir/file"),
325                                  (DefaultAliasMarker, "c:dir/file"))
326         else:
327             self.failUnlessEqual(ga2("c:"), ("CA", ""))
328             self.failUnlessEqual(ga2("c:file"), ("CA", "file"))
329             self.failUnlessEqual(ga2("c:dir/file"), ("CA", "dir/file"))
330         self.failUnlessEqual(ga2("work:"), ("WA", ""))
331         self.failUnlessEqual(ga2("work:file"), ("WA", "file"))
332         self.failUnlessEqual(ga2("work:dir/file"), ("WA", "dir/file"))
333         self.failUnlessEqual(ga2("URI:stuff"), ("URI:stuff", ""))
334         self.failUnlessEqual(ga2("URI:stuff:./file"), ("URI:stuff", "file"))
335         self.failUnlessEqual(ga2("URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
336         self.failUnlessRaises(common.UnknownAliasError, ga2, "missing:")
337         self.failUnlessRaises(common.UnknownAliasError, ga2, "missing:dir")
338         self.failUnlessRaises(common.UnknownAliasError, ga2, "missing:dir/file")
339
340         def ga3(path):
341             old = common.pretend_platform_uses_lettercolon
342             try:
343                 common.pretend_platform_uses_lettercolon = True
344                 retval = get_alias(aliases, path, None)
345             finally:
346                 common.pretend_platform_uses_lettercolon = old
347             return retval
348         self.failUnlessEqual(ga3("bare"), (DefaultAliasMarker, "bare"))
349         self.failUnlessEqual(ga3("baredir/file"),
350                              (DefaultAliasMarker, "baredir/file"))
351         self.failUnlessEqual(ga3("baredir/file:7"),
352                              (DefaultAliasMarker, "baredir/file:7"))
353         self.failUnlessEqual(ga3("baredir/sub:1/file:7"),
354                              (DefaultAliasMarker, "baredir/sub:1/file:7"))
355         self.failUnlessEqual(ga3("tahoe:"), ("TA", ""))
356         self.failUnlessEqual(ga3("tahoe:file"), ("TA", "file"))
357         self.failUnlessEqual(ga3("tahoe:dir/file"), ("TA", "dir/file"))
358         self.failUnlessEqual(ga3("c:"), (DefaultAliasMarker, "c:"))
359         self.failUnlessEqual(ga3("c:file"), (DefaultAliasMarker, "c:file"))
360         self.failUnlessEqual(ga3("c:dir/file"),
361                              (DefaultAliasMarker, "c:dir/file"))
362         self.failUnlessEqual(ga3("work:"), ("WA", ""))
363         self.failUnlessEqual(ga3("work:file"), ("WA", "file"))
364         self.failUnlessEqual(ga3("work:dir/file"), ("WA", "dir/file"))
365         self.failUnlessEqual(ga3("URI:stuff"), ("URI:stuff", ""))
366         self.failUnlessEqual(ga3("URI:stuff:./file"), ("URI:stuff", "file"))
367         self.failUnlessEqual(ga3("URI:stuff:./dir/file"), ("URI:stuff", "dir/file"))
368         self.failUnlessRaises(common.UnknownAliasError, ga3, "missing:")
369         self.failUnlessRaises(common.UnknownAliasError, ga3, "missing:dir")
370         self.failUnlessRaises(common.UnknownAliasError, ga3, "missing:dir/file")
371
372
373 class Help(unittest.TestCase):
374
375     def test_get(self):
376         help = str(cli.GetOptions())
377         self.failUnless("get VDRIVE_FILE LOCAL_FILE" in help, help)
378         self.failUnless("% tahoe get FOO |less" in help, help)
379
380     def test_put(self):
381         help = str(cli.PutOptions())
382         self.failUnless("put LOCAL_FILE VDRIVE_FILE" in help, help)
383         self.failUnless("% cat FILE | tahoe put" in help, help)
384
385     def test_rm(self):
386         help = str(cli.RmOptions())
387         self.failUnless("rm VDRIVE_FILE" in help, help)
388
389     def test_mv(self):
390         help = str(cli.MvOptions())
391         self.failUnless("mv FROM TO" in help, help)
392
393     def test_ln(self):
394         help = str(cli.LnOptions())
395         self.failUnless("ln FROM TO" in help, help)
396
397     def test_backup(self):
398         help = str(cli.BackupOptions())
399         self.failUnless("backup FROM ALIAS:TO" in help, help)
400
401     def test_webopen(self):
402         help = str(cli.WebopenOptions())
403         self.failUnless("webopen [ALIAS:PATH]" in help, help)
404
405     def test_manifest(self):
406         help = str(cli.ManifestOptions())
407         self.failUnless("manifest [ALIAS:PATH]" in help, help)
408
409     def test_stats(self):
410         help = str(cli.StatsOptions())
411         self.failUnless("stats [ALIAS:PATH]" in help, help)
412
413     def test_check(self):
414         help = str(cli.CheckOptions())
415         self.failUnless("check [ALIAS:PATH]" in help, help)
416
417     def test_deep_check(self):
418         help = str(cli.DeepCheckOptions())
419         self.failUnless("deep-check [ALIAS:PATH]" in help, help)
420
421     def test_create_alias(self):
422         help = str(cli.CreateAliasOptions())
423         self.failUnless("create-alias ALIAS" in help, help)
424
425     def test_add_aliases(self):
426         help = str(cli.AddAliasOptions())
427         self.failUnless("add-alias ALIAS DIRCAP" in help, help)
428
429 class CLITestMixin:
430     def do_cli(self, verb, *args, **kwargs):
431         nodeargs = [
432             "--node-directory", self.get_clientdir(),
433             ]
434         argv = [verb] + nodeargs + list(args)
435         stdin = kwargs.get("stdin", "")
436         stdout, stderr = StringIO(), StringIO()
437         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
438                                   stdin=StringIO(stdin),
439                                   stdout=stdout, stderr=stderr)
440         def _done(rc):
441             return rc, stdout.getvalue(), stderr.getvalue()
442         d.addCallback(_done)
443         return d
444
445 class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase):
446
447     def _test_webopen(self, args, expected_url):
448         woo = cli.WebopenOptions()
449         all_args = ["--node-directory", self.get_clientdir()] + list(args)
450         woo.parseOptions(all_args)
451         urls = []
452         rc = cli.webopen(woo, urls.append)
453         self.failUnlessEqual(rc, 0)
454         self.failUnlessEqual(len(urls), 1)
455         self.failUnlessEqual(urls[0], expected_url)
456
457     def test_create(self):
458         self.basedir = os.path.dirname(self.mktemp())
459         self.set_up_grid()
460
461         d = self.do_cli("create-alias", "tahoe")
462         def _done((rc,stdout,stderr)):
463             self.failUnless("Alias 'tahoe' created" in stdout)
464             self.failIf(stderr)
465             aliases = get_aliases(self.get_clientdir())
466             self.failUnless("tahoe" in aliases)
467             self.failUnless(aliases["tahoe"].startswith("URI:DIR2:"))
468         d.addCallback(_done)
469         d.addCallback(lambda res: self.do_cli("create-alias", "two"))
470
471         def _stash_urls(res):
472             aliases = get_aliases(self.get_clientdir())
473             node_url_file = os.path.join(self.get_clientdir(), "node.url")
474             nodeurl = open(node_url_file, "r").read().strip()
475             uribase = nodeurl + "uri/"
476             self.tahoe_url = uribase + urllib.quote(aliases["tahoe"])
477             self.tahoe_subdir_url = self.tahoe_url + "/subdir"
478             self.two_url = uribase + urllib.quote(aliases["two"])
479             self.two_uri = aliases["two"]
480         d.addCallback(_stash_urls)
481
482         d.addCallback(lambda res: self.do_cli("create-alias", "two")) # dup
483         def _check_create_duplicate((rc,stdout,stderr)):
484             self.failIfEqual(rc, 0)
485             self.failUnless("Alias 'two' already exists!" in stderr)
486             aliases = get_aliases(self.get_clientdir())
487             self.failUnlessEqual(aliases["two"], self.two_uri)
488         d.addCallback(_check_create_duplicate)
489
490         d.addCallback(lambda res: self.do_cli("add-alias", "added", self.two_uri))
491         def _check_add((rc,stdout,stderr)):
492             self.failUnlessEqual(rc, 0)
493             self.failUnless("Alias 'added' added" in stdout)
494         d.addCallback(_check_add)
495
496         # check add-alias with a duplicate
497         d.addCallback(lambda res: self.do_cli("add-alias", "two", self.two_uri))
498         def _check_add_duplicate((rc,stdout,stderr)):
499             self.failIfEqual(rc, 0)
500             self.failUnless("Alias 'two' already exists!" in stderr)
501             aliases = get_aliases(self.get_clientdir())
502             self.failUnlessEqual(aliases["two"], self.two_uri)
503         d.addCallback(_check_add_duplicate)
504
505         def _test_urls(junk):
506             self._test_webopen([], self.tahoe_url)
507             self._test_webopen(["/"], self.tahoe_url)
508             self._test_webopen(["tahoe:"], self.tahoe_url)
509             self._test_webopen(["tahoe:/"], self.tahoe_url)
510             self._test_webopen(["tahoe:subdir"], self.tahoe_subdir_url)
511             self._test_webopen(["tahoe:subdir/"], self.tahoe_subdir_url + '/')
512             self._test_webopen(["tahoe:subdir/file"], self.tahoe_subdir_url + '/file')
513             # if "file" is indeed a file, then the url produced by webopen in
514             # this case is disallowed by the webui. but by design, webopen
515             # passes through the mistake from the user to the resultant
516             # webopened url
517             self._test_webopen(["tahoe:subdir/file/"], self.tahoe_subdir_url + '/file/')
518             self._test_webopen(["two:"], self.two_url)
519         d.addCallback(_test_urls)
520
521         return d
522
523 class Put(GridTestMixin, CLITestMixin, unittest.TestCase):
524
525     def test_unlinked_immutable_stdin(self):
526         # tahoe get `echo DATA | tahoe put`
527         # tahoe get `echo DATA | tahoe put -`
528
529         self.basedir = self.mktemp()
530         DATA = "data" * 100
531         self.set_up_grid()
532         d = self.do_cli("put", stdin=DATA)
533         def _uploaded(res):
534             (rc, stdout, stderr) = res
535             self.failUnless("waiting for file data on stdin.." in stderr)
536             self.failUnless("200 OK" in stderr, stderr)
537             self.readcap = stdout
538             self.failUnless(self.readcap.startswith("URI:CHK:"))
539         d.addCallback(_uploaded)
540         d.addCallback(lambda res: self.do_cli("get", self.readcap))
541         def _downloaded(res):
542             (rc, stdout, stderr) = res
543             self.failUnlessEqual(stderr, "")
544             self.failUnlessEqual(stdout, DATA)
545         d.addCallback(_downloaded)
546         d.addCallback(lambda res: self.do_cli("put", "-", stdin=DATA))
547         d.addCallback(lambda (rc,stdout,stderr):
548                       self.failUnlessEqual(stdout, self.readcap))
549         return d
550
551     def test_unlinked_immutable_from_file(self):
552         # tahoe put file.txt
553         # tahoe put ./file.txt
554         # tahoe put /tmp/file.txt
555         # tahoe put ~/file.txt
556         self.basedir = os.path.dirname(self.mktemp())
557         # this will be "allmydata.test.test_cli/Put/test_put_from_file/RANDOM"
558         # and the RANDOM directory will exist. Raw mktemp returns a filename.
559         self.set_up_grid()
560
561         rel_fn = os.path.join(self.basedir, "DATAFILE")
562         abs_fn = os.path.abspath(rel_fn)
563         # we make the file small enough to fit in a LIT file, for speed
564         f = open(rel_fn, "w")
565         f.write("short file")
566         f.close()
567         d = self.do_cli("put", rel_fn)
568         def _uploaded((rc,stdout,stderr)):
569             readcap = stdout
570             self.failUnless(readcap.startswith("URI:LIT:"))
571             self.readcap = readcap
572         d.addCallback(_uploaded)
573         d.addCallback(lambda res: self.do_cli("put", "./" + rel_fn))
574         d.addCallback(lambda (rc,stdout,stderr):
575                       self.failUnlessEqual(stdout, self.readcap))
576         d.addCallback(lambda res: self.do_cli("put", abs_fn))
577         d.addCallback(lambda (rc,stdout,stderr):
578                       self.failUnlessEqual(stdout, self.readcap))
579         # we just have to assume that ~ is handled properly
580         return d
581
582     def test_immutable_from_file(self):
583         # tahoe put file.txt uploaded.txt
584         # tahoe - uploaded.txt
585         # tahoe put file.txt subdir/uploaded.txt
586         # tahoe put file.txt tahoe:uploaded.txt
587         # tahoe put file.txt tahoe:subdir/uploaded.txt
588         # tahoe put file.txt DIRCAP:./uploaded.txt
589         # tahoe put file.txt DIRCAP:./subdir/uploaded.txt
590         self.basedir = os.path.dirname(self.mktemp())
591         self.set_up_grid()
592
593         rel_fn = os.path.join(self.basedir, "DATAFILE")
594         abs_fn = os.path.abspath(rel_fn)
595         # we make the file small enough to fit in a LIT file, for speed
596         DATA = "short file"
597         DATA2 = "short file two"
598         f = open(rel_fn, "w")
599         f.write(DATA)
600         f.close()
601
602         d = self.do_cli("create-alias", "tahoe")
603
604         d.addCallback(lambda res:
605                       self.do_cli("put", rel_fn, "uploaded.txt"))
606         def _uploaded((rc,stdout,stderr)):
607             readcap = stdout.strip()
608             self.failUnless(readcap.startswith("URI:LIT:"))
609             self.failUnless("201 Created" in stderr, stderr)
610             self.readcap = readcap
611         d.addCallback(_uploaded)
612         d.addCallback(lambda res:
613                       self.do_cli("get", "tahoe:uploaded.txt"))
614         d.addCallback(lambda (rc,stdout,stderr):
615                       self.failUnlessEqual(stdout, DATA))
616
617         d.addCallback(lambda res:
618                       self.do_cli("put", "-", "uploaded.txt", stdin=DATA2))
619         def _replaced((rc,stdout,stderr)):
620             readcap = stdout.strip()
621             self.failUnless(readcap.startswith("URI:LIT:"))
622             self.failUnless("200 OK" in stderr, stderr)
623         d.addCallback(_replaced)
624
625         d.addCallback(lambda res:
626                       self.do_cli("put", rel_fn, "subdir/uploaded2.txt"))
627         d.addCallback(lambda res: self.do_cli("get", "subdir/uploaded2.txt"))
628         d.addCallback(lambda (rc,stdout,stderr):
629                       self.failUnlessEqual(stdout, DATA))
630
631         d.addCallback(lambda res:
632                       self.do_cli("put", rel_fn, "tahoe:uploaded3.txt"))
633         d.addCallback(lambda res: self.do_cli("get", "tahoe:uploaded3.txt"))
634         d.addCallback(lambda (rc,stdout,stderr):
635                       self.failUnlessEqual(stdout, DATA))
636
637         d.addCallback(lambda res:
638                       self.do_cli("put", rel_fn, "tahoe:subdir/uploaded4.txt"))
639         d.addCallback(lambda res:
640                       self.do_cli("get", "tahoe:subdir/uploaded4.txt"))
641         d.addCallback(lambda (rc,stdout,stderr):
642                       self.failUnlessEqual(stdout, DATA))
643
644         def _get_dircap(res):
645             self.dircap = get_aliases(self.get_clientdir())["tahoe"]
646         d.addCallback(_get_dircap)
647
648         d.addCallback(lambda res:
649                       self.do_cli("put", rel_fn,
650                                   self.dircap+":./uploaded5.txt"))
651         d.addCallback(lambda res:
652                       self.do_cli("get", "tahoe:uploaded5.txt"))
653         d.addCallback(lambda (rc,stdout,stderr):
654                       self.failUnlessEqual(stdout, DATA))
655
656         d.addCallback(lambda res:
657                       self.do_cli("put", rel_fn,
658                                   self.dircap+":./subdir/uploaded6.txt"))
659         d.addCallback(lambda res:
660                       self.do_cli("get", "tahoe:subdir/uploaded6.txt"))
661         d.addCallback(lambda (rc,stdout,stderr):
662                       self.failUnlessEqual(stdout, DATA))
663
664         return d
665
666     def test_mutable_unlinked(self):
667         # FILECAP = `echo DATA | tahoe put --mutable`
668         # tahoe get FILECAP, compare against DATA
669         # echo DATA2 | tahoe put - FILECAP
670         # tahoe get FILECAP, compare against DATA2
671         # tahoe put file.txt FILECAP
672         self.basedir = os.path.dirname(self.mktemp())
673         self.set_up_grid()
674
675         DATA = "data" * 100
676         DATA2 = "two" * 100
677         rel_fn = os.path.join(self.basedir, "DATAFILE")
678         abs_fn = os.path.abspath(rel_fn)
679         DATA3 = "three" * 100
680         f = open(rel_fn, "w")
681         f.write(DATA3)
682         f.close()
683
684         d = self.do_cli("put", "--mutable", stdin=DATA)
685         def _created(res):
686             (rc, stdout, stderr) = res
687             self.failUnless("waiting for file data on stdin.." in stderr)
688             self.failUnless("200 OK" in stderr)
689             self.filecap = stdout
690             self.failUnless(self.filecap.startswith("URI:SSK:"))
691         d.addCallback(_created)
692         d.addCallback(lambda res: self.do_cli("get", self.filecap))
693         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA))
694
695         d.addCallback(lambda res: self.do_cli("put", "-", self.filecap, stdin=DATA2))
696         def _replaced(res):
697             (rc, stdout, stderr) = res
698             self.failUnless("waiting for file data on stdin.." in stderr)
699             self.failUnless("200 OK" in stderr)
700             self.failUnlessEqual(self.filecap, stdout)
701         d.addCallback(_replaced)
702         d.addCallback(lambda res: self.do_cli("get", self.filecap))
703         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA2))
704
705         d.addCallback(lambda res: self.do_cli("put", rel_fn, self.filecap))
706         def _replaced2(res):
707             (rc, stdout, stderr) = res
708             self.failUnless("200 OK" in stderr)
709             self.failUnlessEqual(self.filecap, stdout)
710         d.addCallback(_replaced2)
711         d.addCallback(lambda res: self.do_cli("get", self.filecap))
712         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA3))
713
714         return d
715
716     def test_mutable(self):
717         # echo DATA1 | tahoe put --mutable - uploaded.txt
718         # echo DATA2 | tahoe put - uploaded.txt # should modify-in-place
719         # tahoe get uploaded.txt, compare against DATA2
720
721         self.basedir = os.path.dirname(self.mktemp())
722         self.set_up_grid()
723
724         DATA1 = "data" * 100
725         fn1 = os.path.join(self.basedir, "DATA1")
726         f = open(fn1, "w")
727         f.write(DATA1)
728         f.close()
729         DATA2 = "two" * 100
730         fn2 = os.path.join(self.basedir, "DATA2")
731         f = open(fn2, "w")
732         f.write(DATA2)
733         f.close()
734
735         d = self.do_cli("create-alias", "tahoe")
736         d.addCallback(lambda res:
737                       self.do_cli("put", "--mutable", fn1, "tahoe:uploaded.txt"))
738         d.addCallback(lambda res:
739                       self.do_cli("put", fn2, "tahoe:uploaded.txt"))
740         d.addCallback(lambda res:
741                       self.do_cli("get", "tahoe:uploaded.txt"))
742         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA2))
743         return d
744
745 class Cp(GridTestMixin, CLITestMixin, unittest.TestCase):
746
747     def test_not_enough_args(self):
748         o = cli.CpOptions()
749         self.failUnlessRaises(usage.UsageError,
750                               o.parseOptions, ["onearg"])
751
752     def test_unicode_filename(self):
753         self.basedir = os.path.dirname(self.mktemp())
754         self.set_up_grid()
755
756         fn1 = os.path.join(self.basedir, "Ärtonwall")
757         DATA1 = "unicode file content"
758         open(fn1, "wb").write(DATA1)
759
760         fn2 = os.path.join(self.basedir, "Metallica")
761         DATA2 = "non-unicode file content"
762         open(fn2, "wb").write(DATA2)
763
764         # Bug #534
765         # Assure that uploading a file whose name contains unicode character doesn't
766         # prevent further uploads in the same directory
767         d = self.do_cli("create-alias", "tahoe")
768         d.addCallback(lambda res: self.do_cli("cp", fn1, "tahoe:"))
769         d.addCallback(lambda res: self.do_cli("cp", fn2, "tahoe:"))
770
771         d.addCallback(lambda res: self.do_cli("get", "tahoe:Ärtonwall"))
772         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA1))
773
774         d.addCallback(lambda res: self.do_cli("get", "tahoe:Metallica"))
775         d.addCallback(lambda (rc,out,err): self.failUnlessEqual(out, DATA2))
776
777         return d
778     test_unicode_filename.todo = "This behavior is not yet supported, although it does happen to work (for reasons that are ill-understood) on many platforms.  See issue ticket #534."
779
780     def test_dangling_symlink_vs_recursion(self):
781         if not hasattr(os, 'symlink'):
782             raise unittest.SkipTest("There is no symlink on this platform.")
783         # cp -r on a directory containing a dangling symlink shouldn't assert
784         self.basedir = os.path.dirname(self.mktemp())
785         self.set_up_grid()
786         dn = os.path.join(self.basedir, "dir")
787         os.mkdir(dn)
788         fn = os.path.join(dn, "Fakebandica")
789         ln = os.path.join(dn, "link")
790         os.symlink(fn, ln)
791
792         d = self.do_cli("create-alias", "tahoe")
793         d.addCallback(lambda res: self.do_cli("cp", "--recursive",
794                                               dn, "tahoe:"))
795         return d
796
797 class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
798
799     def writeto(self, path, data):
800         d = os.path.dirname(os.path.join(self.basedir, "home", path))
801         fileutil.make_dirs(d)
802         f = open(os.path.join(self.basedir, "home", path), "w")
803         f.write(data)
804         f.close()
805
806     def count_output(self, out):
807         mo = re.search(r"(\d)+ files uploaded \((\d+) reused\), (\d+) directories created \((\d+) reused\)", out)
808         return [int(s) for s in mo.groups()]
809
810     def count_output2(self, out):
811         mo = re.search(r"(\d)+ files checked, (\d+) directories checked, (\d+) directories read", out)
812         return [int(s) for s in mo.groups()]
813
814     def test_backup(self):
815         self.basedir = os.path.dirname(self.mktemp())
816         self.set_up_grid()
817
818         # is the backupdb available? If so, we test that a second backup does
819         # not create new directories.
820         hush = StringIO()
821         have_bdb = backupdb.get_backupdb(os.path.join(self.basedir, "dbtest"),
822                                          hush)
823
824         # create a small local directory with a couple of files
825         source = os.path.join(self.basedir, "home")
826         fileutil.make_dirs(os.path.join(source, "empty"))
827         self.writeto("parent/subdir/foo.txt", "foo")
828         self.writeto("parent/subdir/bar.txt", "bar\n" * 1000)
829         self.writeto("parent/blah.txt", "blah")
830
831         def do_backup(use_backupdb=True, verbose=False):
832             cmd = ["backup"]
833             if not have_bdb or not use_backupdb:
834                 cmd.append("--no-backupdb")
835             if verbose:
836                 cmd.append("--verbose")
837             cmd.append(source)
838             cmd.append("tahoe:backups")
839             return self.do_cli(*cmd)
840
841         d = self.do_cli("create-alias", "tahoe")
842
843         if not have_bdb:
844             d.addCallback(lambda res: self.do_cli("backup", source, "tahoe:backups"))
845             def _should_complain((rc, out, err)):
846                 self.failUnless("I was unable to import a python sqlite library" in err, err)
847             d.addCallback(_should_complain)
848             d.addCallback(self.stall, 1.1) # make sure the backups get distinct timestamps
849
850         d.addCallback(lambda res: do_backup())
851         def _check0((rc, out, err)):
852             self.failUnlessEqual(err, "")
853             self.failUnlessEqual(rc, 0)
854             fu, fr, dc, dr = self.count_output(out)
855             # foo.txt, bar.txt, blah.txt
856             self.failUnlessEqual(fu, 3)
857             self.failUnlessEqual(fr, 0)
858             # empty, home, home/parent, home/parent/subdir
859             self.failUnlessEqual(dc, 4)
860             self.failUnlessEqual(dr, 0)
861         d.addCallback(_check0)
862
863         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups"))
864         def _check1((rc, out, err)):
865             self.failUnlessEqual(err, "")
866             self.failUnlessEqual(rc, 0)
867             self.failUnlessEqual(sorted(out.split()), ["Archives", "Latest"])
868         d.addCallback(_check1)
869         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Latest"))
870         def _check2((rc, out, err)):
871             self.failUnlessEqual(err, "")
872             self.failUnlessEqual(rc, 0)
873             self.failUnlessEqual(sorted(out.split()), ["empty", "parent"])
874         d.addCallback(_check2)
875         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Latest/empty"))
876         def _check2a((rc, out, err)):
877             self.failUnlessEqual(err, "")
878             self.failUnlessEqual(rc, 0)
879             self.failUnlessEqual(out.strip(), "")
880         d.addCallback(_check2a)
881         d.addCallback(lambda res: self.do_cli("get", "tahoe:backups/Latest/parent/subdir/foo.txt"))
882         def _check3((rc, out, err)):
883             self.failUnlessEqual(err, "")
884             self.failUnlessEqual(rc, 0)
885             self.failUnlessEqual(out, "foo")
886         d.addCallback(_check3)
887         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
888         def _check4((rc, out, err)):
889             self.failUnlessEqual(err, "")
890             self.failUnlessEqual(rc, 0)
891             self.old_archives = out.split()
892             self.failUnlessEqual(len(self.old_archives), 1)
893         d.addCallback(_check4)
894
895
896         d.addCallback(self.stall, 1.1)
897         d.addCallback(lambda res: do_backup())
898         def _check4a((rc, out, err)):
899             # second backup should reuse everything, if the backupdb is
900             # available
901             self.failUnlessEqual(err, "")
902             self.failUnlessEqual(rc, 0)
903             if have_bdb:
904                 fu, fr, dc, dr = self.count_output(out)
905                 # foo.txt, bar.txt, blah.txt
906                 self.failUnlessEqual(fu, 0)
907                 self.failUnlessEqual(fr, 3)
908                 # empty, home, home/parent, home/parent/subdir
909                 self.failUnlessEqual(dc, 0)
910                 self.failUnlessEqual(dr, 4)
911         d.addCallback(_check4a)
912
913         if have_bdb:
914             # sneak into the backupdb, crank back the "last checked"
915             # timestamp to force a check on all files
916             def _reset_last_checked(res):
917                 dbfile = os.path.join(self.get_clientdir(),
918                                       "private", "backupdb.sqlite")
919                 self.failUnless(os.path.exists(dbfile), dbfile)
920                 bdb = backupdb.get_backupdb(dbfile)
921                 bdb.cursor.execute("UPDATE last_upload SET last_checked=0")
922                 bdb.connection.commit()
923
924             d.addCallback(_reset_last_checked)
925
926             d.addCallback(self.stall, 1.1)
927             d.addCallback(lambda res: do_backup(verbose=True))
928             def _check4b((rc, out, err)):
929                 # we should check all files, and re-use all of them. None of
930                 # the directories should have been changed.
931                 self.failUnlessEqual(err, "")
932                 self.failUnlessEqual(rc, 0)
933                 fu, fr, dc, dr = self.count_output(out)
934                 fchecked, dchecked, dread = self.count_output2(out)
935                 self.failUnlessEqual(fchecked, 3)
936                 self.failUnlessEqual(fu, 0)
937                 self.failUnlessEqual(fr, 3)
938                 # TODO: backupdb doesn't do dirs yet; when it does, this will
939                 # change to dchecked=4, and maybe dread=0
940                 self.failUnlessEqual(dchecked, 0)
941                 self.failUnlessEqual(dread, 4)
942                 self.failUnlessEqual(dc, 0)
943                 self.failUnlessEqual(dr, 4)
944             d.addCallback(_check4b)
945
946         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
947         def _check5((rc, out, err)):
948             self.failUnlessEqual(err, "")
949             self.failUnlessEqual(rc, 0)
950             self.new_archives = out.split()
951             expected_new = 2
952             if have_bdb:
953                 expected_new += 1
954             self.failUnlessEqual(len(self.new_archives), expected_new, out)
955             # the original backup should still be the oldest (i.e. sorts
956             # alphabetically towards the beginning)
957             self.failUnlessEqual(sorted(self.new_archives)[0],
958                                  self.old_archives[0])
959         d.addCallback(_check5)
960
961         d.addCallback(self.stall, 1.1)
962         def _modify(res):
963             self.writeto("parent/subdir/foo.txt", "FOOF!")
964             # and turn a file into a directory
965             os.unlink(os.path.join(source, "parent/blah.txt"))
966             os.mkdir(os.path.join(source, "parent/blah.txt"))
967             self.writeto("parent/blah.txt/surprise file", "surprise")
968             self.writeto("parent/blah.txt/surprisedir/subfile", "surprise")
969             # turn a directory into a file
970             os.rmdir(os.path.join(source, "empty"))
971             self.writeto("empty", "imagine nothing being here")
972             return do_backup()
973         d.addCallback(_modify)
974         def _check5a((rc, out, err)):
975             # second backup should reuse bar.txt (if backupdb is available),
976             # and upload the rest. None of the directories can be reused.
977             self.failUnlessEqual(err, "")
978             self.failUnlessEqual(rc, 0)
979             if have_bdb:
980                 fu, fr, dc, dr = self.count_output(out)
981                 # new foo.txt, surprise file, subfile, empty
982                 self.failUnlessEqual(fu, 4)
983                 # old bar.txt
984                 self.failUnlessEqual(fr, 1)
985                 # home, parent, subdir, blah.txt, surprisedir
986                 self.failUnlessEqual(dc, 5)
987                 self.failUnlessEqual(dr, 0)
988         d.addCallback(_check5a)
989         d.addCallback(lambda res: self.do_cli("ls", "tahoe:backups/Archives"))
990         def _check6((rc, out, err)):
991             self.failUnlessEqual(err, "")
992             self.failUnlessEqual(rc, 0)
993             self.new_archives = out.split()
994             expected_new = 3
995             if have_bdb:
996                 expected_new += 1
997             self.failUnlessEqual(len(self.new_archives), expected_new)
998             self.failUnlessEqual(sorted(self.new_archives)[0],
999                                  self.old_archives[0])
1000         d.addCallback(_check6)
1001         d.addCallback(lambda res: self.do_cli("get", "tahoe:backups/Latest/parent/subdir/foo.txt"))
1002         def _check7((rc, out, err)):
1003             self.failUnlessEqual(err, "")
1004             self.failUnlessEqual(rc, 0)
1005             self.failUnlessEqual(out, "FOOF!")
1006             # the old snapshot should not be modified
1007             return self.do_cli("get", "tahoe:backups/Archives/%s/parent/subdir/foo.txt" % self.old_archives[0])
1008         d.addCallback(_check7)
1009         def _check8((rc, out, err)):
1010             self.failUnlessEqual(err, "")
1011             self.failUnlessEqual(rc, 0)
1012             self.failUnlessEqual(out, "foo")
1013         d.addCallback(_check8)
1014
1015         d.addCallback(self.stall, 1.1)
1016         d.addCallback(lambda res: do_backup(use_backupdb=False))
1017         def _check9((rc, out, err)):
1018             # --no-backupdb means re-upload everything. We still get to
1019             # re-use the directories, since nothing changed.
1020             self.failUnlessEqual(err, "")
1021             self.failUnlessEqual(rc, 0)
1022             fu, fr, dc, dr = self.count_output(out)
1023             self.failUnlessEqual(fu, 5)
1024             self.failUnlessEqual(fr, 0)
1025             self.failUnlessEqual(dc, 0)
1026             self.failUnlessEqual(dr, 5)
1027         d.addCallback(_check9)
1028
1029         return d
1030
1031     # on our old dapper buildslave, this test takes a long time (usually
1032     # 130s), so we have to bump up the default 120s timeout. The create-alias
1033     # and initial backup alone take 60s, probably because of the handful of
1034     # dirnodes being created (RSA key generation). The backup between check4
1035     # and check4a takes 6s, as does the backup before check4b.
1036     test_backup.timeout = 300
1037
1038     def test_exclude_options(self):
1039         root_listdir = ('lib.a', '_darcs', 'subdir', 'nice_doc.lyx')
1040         subdir_listdir = ('another_doc.lyx', 'run_snake_run.py', 'CVS', '.svn', '_darcs')
1041         basedir = os.path.dirname(self.mktemp())
1042         nodeurl_path = os.path.join(basedir, 'node.url')
1043         nodeurl = file(nodeurl_path, 'w')
1044         nodeurl.write('http://example.net:2357/')
1045         nodeurl.close()
1046
1047         def _check_filtering(filtered, all, included, excluded):
1048             filtered = set(filtered)
1049             all = set(all)
1050             included = set(included)
1051             excluded = set(excluded)
1052             self.failUnlessEqual(filtered, included)
1053             self.failUnlessEqual(all.difference(filtered), excluded)
1054
1055         # test simple exclude
1056         backup_options = cli.BackupOptions()
1057         backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
1058                                      basedir, 'from', 'to'])
1059         filtered = list(backup_options.filter_listdir(root_listdir))
1060         _check_filtering(filtered, root_listdir, ('lib.a', '_darcs', 'subdir'),
1061                          ('nice_doc.lyx',))
1062         # multiple exclude
1063         backup_options = cli.BackupOptions()
1064         backup_options.parseOptions(['--exclude', '*lyx', '--exclude', 'lib.?', '--node-directory',
1065                                      basedir, 'from', 'to'])
1066         filtered = list(backup_options.filter_listdir(root_listdir))
1067         _check_filtering(filtered, root_listdir, ('_darcs', 'subdir'),
1068                          ('nice_doc.lyx', 'lib.a'))
1069         # vcs metadata exclusion
1070         backup_options = cli.BackupOptions()
1071         backup_options.parseOptions(['--exclude-vcs', '--node-directory',
1072                                      basedir, 'from', 'to'])
1073         filtered = list(backup_options.filter_listdir(subdir_listdir))
1074         _check_filtering(filtered, subdir_listdir, ('another_doc.lyx', 'run_snake_run.py',),
1075                          ('CVS', '.svn', '_darcs'))
1076         # read exclude patterns from file
1077         exclusion_string = "_darcs\n*py\n.svn"
1078         excl_filepath = os.path.join(basedir, 'exclusion')
1079         excl_file = file(excl_filepath, 'w')
1080         excl_file.write(exclusion_string)
1081         excl_file.close()
1082         backup_options = cli.BackupOptions()
1083         backup_options.parseOptions(['--exclude-from', excl_filepath, '--node-directory',
1084                                      basedir, 'from', 'to'])
1085         filtered = list(backup_options.filter_listdir(subdir_listdir))
1086         _check_filtering(filtered, subdir_listdir, ('another_doc.lyx', 'CVS'),
1087                          ('.svn', '_darcs', 'run_snake_run.py'))
1088         # text BackupConfigurationError
1089         self.failUnlessRaises(cli.BackupConfigurationError,
1090                               backup_options.parseOptions,
1091                               ['--exclude-from', excl_filepath + '.no', '--node-directory',
1092                                basedir, 'from', 'to'])
1093
1094         # test that an iterator works too
1095         backup_options = cli.BackupOptions()
1096         backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
1097                                      basedir, 'from', 'to'])
1098         filtered = list(backup_options.filter_listdir(iter(root_listdir)))
1099         _check_filtering(filtered, root_listdir, ('lib.a', '_darcs', 'subdir'),
1100                          ('nice_doc.lyx',))
1101
1102 class Check(GridTestMixin, CLITestMixin, unittest.TestCase):
1103
1104     def test_check(self):
1105         self.basedir = "cli/Check/check"
1106         self.set_up_grid()
1107         c0 = self.g.clients[0]
1108         DATA = "data" * 100
1109         d = c0.create_mutable_file(DATA)
1110         def _stash_uri(n):
1111             self.uri = n.get_uri()
1112         d.addCallback(_stash_uri)
1113
1114         d.addCallback(lambda ign: self.do_cli("check", self.uri))
1115         def _check1((rc, out, err)):
1116             self.failUnlessEqual(err, "")
1117             self.failUnlessEqual(rc, 0)
1118             lines = out.splitlines()
1119             self.failUnless("Summary: Healthy" in lines, out)
1120             self.failUnless(" good-shares: 10 (encoding is 3-of-10)" in lines, out)
1121         d.addCallback(_check1)
1122
1123         d.addCallback(lambda ign: self.do_cli("check", "--raw", self.uri))
1124         def _check2((rc, out, err)):
1125             self.failUnlessEqual(err, "")
1126             self.failUnlessEqual(rc, 0)
1127             data = simplejson.loads(out)
1128             self.failUnlessEqual(data["summary"], "Healthy")
1129         d.addCallback(_check2)
1130
1131         def _clobber_shares(ignored):
1132             # delete one, corrupt a second
1133             shares = self.find_shares(self.uri)
1134             self.failUnlessEqual(len(shares), 10)
1135             os.unlink(shares[0][2])
1136             cso = debug.CorruptShareOptions()
1137             cso.stdout = StringIO()
1138             cso.parseOptions([shares[1][2]])
1139             storage_index = uri.from_string(self.uri).get_storage_index()
1140             self._corrupt_share_line = "  server %s, SI %s, shnum %d" % \
1141                                        (base32.b2a(shares[1][1]),
1142                                         base32.b2a(storage_index),
1143                                         shares[1][0])
1144             debug.corrupt_share(cso)
1145         d.addCallback(_clobber_shares)
1146
1147         d.addCallback(lambda ign: self.do_cli("check", "--verify", self.uri))
1148         def _check3((rc, out, err)):
1149             self.failUnlessEqual(err, "")
1150             self.failUnlessEqual(rc, 0)
1151             lines = out.splitlines()
1152             summary = [l for l in lines if l.startswith("Summary")][0]
1153             self.failUnless("Summary: Unhealthy: 8 shares (enc 3-of-10)"
1154                             in summary, summary)
1155             self.failUnless(" good-shares: 8 (encoding is 3-of-10)" in lines, out)
1156             self.failUnless(" corrupt shares:" in lines, out)
1157             self.failUnless(self._corrupt_share_line in lines, out)
1158         d.addCallback(_check3)
1159
1160         d.addCallback(lambda ign:
1161                       self.do_cli("check", "--verify", "--repair", self.uri))
1162         def _check4((rc, out, err)):
1163             self.failUnlessEqual(err, "")
1164             self.failUnlessEqual(rc, 0)
1165             lines = out.splitlines()
1166             self.failUnless("Summary: not healthy" in lines, out)
1167             self.failUnless(" good-shares: 8 (encoding is 3-of-10)" in lines, out)
1168             self.failUnless(" corrupt shares:" in lines, out)
1169             self.failUnless(self._corrupt_share_line in lines, out)
1170             self.failUnless(" repair successful" in lines, out)
1171         d.addCallback(_check4)
1172
1173         d.addCallback(lambda ign:
1174                       self.do_cli("check", "--verify", "--repair", self.uri))
1175         def _check5((rc, out, err)):
1176             self.failUnlessEqual(err, "")
1177             self.failUnlessEqual(rc, 0)
1178             lines = out.splitlines()
1179             self.failUnless("Summary: healthy" in lines, out)
1180             self.failUnless(" good-shares: 10 (encoding is 3-of-10)" in lines, out)
1181             self.failIf(" corrupt shares:" in lines, out)
1182         d.addCallback(_check5)
1183
1184         return d
1185
1186     def test_deep_check(self):
1187         self.basedir = "cli/Check/deep_check"
1188         self.set_up_grid()
1189         c0 = self.g.clients[0]
1190         self.uris = {}
1191         self.fileurls = {}
1192         DATA = "data" * 100
1193         d = c0.create_empty_dirnode()
1194         def _stash_root_and_create_file(n):
1195             self.rootnode = n
1196             self.rooturi = n.get_uri()
1197             return n.add_file(u"good", upload.Data(DATA, convergence=""))
1198         d.addCallback(_stash_root_and_create_file)
1199         def _stash_uri(fn, which):
1200             self.uris[which] = fn.get_uri()
1201             return fn
1202         d.addCallback(_stash_uri, "good")
1203         d.addCallback(lambda ign:
1204                       self.rootnode.add_file(u"small",
1205                                              upload.Data("literal",
1206                                                         convergence="")))
1207         d.addCallback(_stash_uri, "small")
1208         d.addCallback(lambda ign: c0.create_mutable_file(DATA+"1"))
1209         d.addCallback(lambda fn: self.rootnode.set_node(u"mutable", fn))
1210         d.addCallback(_stash_uri, "mutable")
1211
1212         d.addCallback(lambda ign: self.do_cli("deep-check", self.rooturi))
1213         def _check1((rc, out, err)):
1214             self.failUnlessEqual(err, "")
1215             self.failUnlessEqual(rc, 0)
1216             lines = out.splitlines()
1217             self.failUnless("done: 4 objects checked, 4 healthy, 0 unhealthy"
1218                             in lines, out)
1219         d.addCallback(_check1)
1220
1221         # root
1222         # root/good
1223         # root/small
1224         # root/mutable
1225
1226         d.addCallback(lambda ign: self.do_cli("deep-check", "--verbose",
1227                                               self.rooturi))
1228         def _check2((rc, out, err)):
1229             self.failUnlessEqual(err, "")
1230             self.failUnlessEqual(rc, 0)
1231             lines = out.splitlines()
1232             self.failUnless("<root>: Healthy" in lines, out)
1233             self.failUnless("small: Healthy (LIT)" in lines, out)
1234             self.failUnless("good: Healthy" in lines, out)
1235             self.failUnless("mutable: Healthy" in lines, out)
1236             self.failUnless("done: 4 objects checked, 4 healthy, 0 unhealthy"
1237                             in lines, out)
1238         d.addCallback(_check2)
1239
1240         def _clobber_shares(ignored):
1241             shares = self.find_shares(self.uris["good"])
1242             self.failUnlessEqual(len(shares), 10)
1243             os.unlink(shares[0][2])
1244
1245             shares = self.find_shares(self.uris["mutable"])
1246             cso = debug.CorruptShareOptions()
1247             cso.stdout = StringIO()
1248             cso.parseOptions([shares[1][2]])
1249             storage_index = uri.from_string(self.uris["mutable"]).get_storage_index()
1250             self._corrupt_share_line = " corrupt: server %s, SI %s, shnum %d" % \
1251                                        (base32.b2a(shares[1][1]),
1252                                         base32.b2a(storage_index),
1253                                         shares[1][0])
1254             debug.corrupt_share(cso)
1255         d.addCallback(_clobber_shares)
1256
1257         # root
1258         # root/good  [9 shares]
1259         # root/small
1260         # root/mutable [1 corrupt share]
1261
1262         d.addCallback(lambda ign:
1263                       self.do_cli("deep-check", "--verbose", self.rooturi))
1264         def _check3((rc, out, err)):
1265             self.failUnlessEqual(err, "")
1266             self.failUnlessEqual(rc, 0)
1267             lines = out.splitlines()
1268             self.failUnless("<root>: Healthy" in lines, out)
1269             self.failUnless("small: Healthy (LIT)" in lines, out)
1270             self.failUnless("mutable: Healthy" in lines, out) # needs verifier
1271             self.failUnless("good: Not Healthy: 9 shares (enc 3-of-10)"
1272                             in lines, out)
1273             self.failIf(self._corrupt_share_line in lines, out)
1274             self.failUnless("done: 4 objects checked, 3 healthy, 1 unhealthy"
1275                             in lines, out)
1276         d.addCallback(_check3)
1277
1278         d.addCallback(lambda ign:
1279                       self.do_cli("deep-check", "--verbose", "--verify",
1280                                   self.rooturi))
1281         def _check4((rc, out, err)):
1282             self.failUnlessEqual(err, "")
1283             self.failUnlessEqual(rc, 0)
1284             lines = out.splitlines()
1285             self.failUnless("<root>: Healthy" in lines, out)
1286             self.failUnless("small: Healthy (LIT)" in lines, out)
1287             mutable = [l for l in lines if l.startswith("mutable")][0]
1288             self.failUnless(mutable.startswith("mutable: Unhealthy: 9 shares (enc 3-of-10)"),
1289                             mutable)
1290             self.failUnless(self._corrupt_share_line in lines, out)
1291             self.failUnless("good: Not Healthy: 9 shares (enc 3-of-10)"
1292                             in lines, out)
1293             self.failUnless("done: 4 objects checked, 2 healthy, 2 unhealthy"
1294                             in lines, out)
1295         d.addCallback(_check4)
1296
1297         d.addCallback(lambda ign:
1298                       self.do_cli("deep-check", "--raw",
1299                                   self.rooturi))
1300         def _check5((rc, out, err)):
1301             self.failUnlessEqual(err, "")
1302             self.failUnlessEqual(rc, 0)
1303             lines = out.splitlines()
1304             units = [simplejson.loads(line) for line in lines]
1305             # root, small, good, mutable,  stats
1306             self.failUnlessEqual(len(units), 4+1)
1307         d.addCallback(_check5)
1308
1309         d.addCallback(lambda ign:
1310                       self.do_cli("deep-check",
1311                                   "--verbose", "--verify", "--repair",
1312                                   self.rooturi))
1313         def _check6((rc, out, err)):
1314             self.failUnlessEqual(err, "")
1315             self.failUnlessEqual(rc, 0)
1316             lines = out.splitlines()
1317             self.failUnless("<root>: healthy" in lines, out)
1318             self.failUnless("small: healthy" in lines, out)
1319             self.failUnless("mutable: not healthy" in lines, out)
1320             self.failUnless(self._corrupt_share_line in lines, out)
1321             self.failUnless("good: not healthy" in lines, out)
1322             self.failUnless("done: 4 objects checked" in lines, out)
1323             self.failUnless(" pre-repair: 2 healthy, 2 unhealthy" in lines, out)
1324             self.failUnless(" 2 repairs attempted, 2 successful, 0 failed"
1325                             in lines, out)
1326             self.failUnless(" post-repair: 4 healthy, 0 unhealthy" in lines,out)
1327         d.addCallback(_check6)
1328
1329         # now add a subdir, and a file below that, then make the subdir
1330         # unrecoverable
1331
1332         d.addCallback(lambda ign:
1333                       self.rootnode.create_empty_directory(u"subdir"))
1334         d.addCallback(_stash_uri, "subdir")
1335         d.addCallback(lambda fn:
1336                       fn.add_file(u"subfile", upload.Data(DATA+"2", "")))
1337         d.addCallback(lambda ign:
1338                       self.delete_shares_numbered(self.uris["subdir"],
1339                                                   range(10)))
1340
1341         # root
1342         # root/good
1343         # root/small
1344         # root/mutable
1345         # root/subdir [unrecoverable: 0 shares]
1346         # root/subfile
1347
1348         d.addCallback(lambda ign: self.do_cli("manifest", self.rooturi))
1349         def _manifest_failed((rc, out, err)):
1350             self.failIfEqual(rc, 0)
1351             self.failUnlessIn("ERROR: ", err)
1352             self.failUnlessIn("UnrecoverableFileError", err)
1353             # the fatal directory should still show up, as the last line
1354             self.failUnlessIn(" subdir\n", out)
1355         d.addCallback(_manifest_failed)
1356
1357         d.addCallback(lambda ign: self.do_cli("deep-check", self.rooturi))
1358         def _deep_check_failed((rc, out, err)):
1359             self.failIfEqual(rc, 0)
1360             self.failUnlessIn("ERROR: ", err)
1361             self.failUnlessIn("UnrecoverableFileError", err)
1362             # we want to make sure that the error indication is the last
1363             # thing that gets emitted
1364             self.failIf("done:" in out, out)
1365         d.addCallback(_deep_check_failed)
1366
1367         # this test is disabled until the deep-repair response to an
1368         # unrepairable directory is fixed. The failure-to-repair should not
1369         # throw an exception, but the failure-to-traverse that follows
1370         # should throw UnrecoverableFileError.
1371
1372         #d.addCallback(lambda ign:
1373         #              self.do_cli("deep-check", "--repair", self.rooturi))
1374         #def _deep_check_repair_failed((rc, out, err)):
1375         #    self.failIfEqual(rc, 0)
1376         #    print err
1377         #    self.failUnlessIn("ERROR: ", err)
1378         #    self.failUnlessIn("UnrecoverableFileError", err)
1379         #    self.failIf("done:" in out, out)
1380         #d.addCallback(_deep_check_repair_failed)
1381
1382         return d
1383